1 /*
2  * 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.BorderStroke
20 import androidx.compose.foundation.background
21 import androidx.compose.foundation.border
22 import androidx.compose.foundation.clickable
23 import androidx.compose.foundation.interaction.Interaction
24 import androidx.compose.foundation.interaction.MutableInteractionSource
25 import androidx.compose.foundation.layout.Box
26 import androidx.compose.foundation.selection.selectable
27 import androidx.compose.foundation.selection.toggleable
28 import androidx.compose.material3.internal.childSemantics
29 import androidx.compose.runtime.Composable
30 import androidx.compose.runtime.CompositionLocalProvider
31 import androidx.compose.runtime.NonRestartableComposable
32 import androidx.compose.runtime.Stable
33 import androidx.compose.runtime.compositionLocalOf
34 import androidx.compose.runtime.remember
35 import androidx.compose.ui.Modifier
36 import androidx.compose.ui.draw.clip
37 import androidx.compose.ui.graphics.Color
38 import androidx.compose.ui.graphics.RectangleShape
39 import androidx.compose.ui.graphics.Shape
40 import androidx.compose.ui.graphics.graphicsLayer
41 import androidx.compose.ui.input.pointer.pointerInput
42 import androidx.compose.ui.platform.LocalDensity
43 import androidx.compose.ui.semantics.isContainer
44 import androidx.compose.ui.semantics.semantics
45 import androidx.compose.ui.unit.Dp
46 import androidx.compose.ui.unit.dp
47 
48 // TODO(b/197880751): Add url to spec on Material.io.
49 /**
50  * Material surface is the central metaphor in material design. Each surface exists at a given
51  * elevation, which influences how that piece of surface visually relates to other surfaces and how
52  * that surface is modified by tonal variance.
53  *
54  * See the other overloads for clickable, selectable, and toggleable surfaces.
55  *
56  * The Surface is responsible for:
57  * 1) Clipping: Surface clips its children to the shape specified by [shape]
58  * 2) Borders: If [shape] has a border, then it will also be drawn.
59  * 3) Background: Surface fills the shape specified by [shape] with the [color]. If [color] is
60  *    [ColorScheme.surface] a color overlay will be applied. The color of the overlay depends on the
61  *    [tonalElevation] of this Surface, and the [LocalAbsoluteTonalElevation] set by any parent
62  *    surfaces. This ensures that a Surface never appears to have a lower elevation overlay than its
63  *    ancestors, by summing the elevation of all previous Surfaces.
64  * 4) Content color: Surface uses [contentColor] to specify a preferred color for the content of
65  *    this surface - this is used by the [Text] and [Icon] components as a default color.
66  *
67  * If no [contentColor] is set, this surface will try and match its background color to a color
68  * defined in the theme [ColorScheme], and return the corresponding content color. For example, if
69  * the [color] of this surface is [ColorScheme.surface], [contentColor] will be set to
70  * [ColorScheme.onSurface]. If [color] is not part of the theme palette, [contentColor] will keep
71  * the same value set above this Surface.
72  *
73  * To manually retrieve the content color inside a surface, use [LocalContentColor].
74  * 5) Blocking touch propagation behind the surface.
75  *
76  * Surface sample:
77  *
78  * @sample androidx.compose.material3.samples.SurfaceSample
79  * @param modifier Modifier to be applied to the layout corresponding to the surface
80  * @param shape Defines the surface's shape as well its shadow.
81  * @param color The background color. Use [Color.Transparent] to have no color.
82  * @param contentColor The preferred content color provided by this Surface to its children.
83  *   Defaults to either the matching content color for [color], or if [color] is not a color from
84  *   the theme, this will keep the same value set above this Surface.
85  * @param tonalElevation When [color] is [ColorScheme.surface], a higher the elevation will result
86  *   in a darker color in light theme and lighter color in dark theme.
87  * @param shadowElevation The size of the shadow below the surface. To prevent shadow creep, only
88  *   apply shadow elevation when absolutely necessary, such as when the surface requires visual
89  *   separation from a patterned background. Note that It will not affect z index of the Surface. If
90  *   you want to change the drawing order you can use `Modifier.zIndex`.
91  * @param border Optional border to draw on top of the surface
92  * @param content The content to be displayed on this Surface
93  */
94 @Composable
95 @NonRestartableComposable
Surfacenull96 fun Surface(
97     modifier: Modifier = Modifier,
98     shape: Shape = RectangleShape,
99     color: Color = MaterialTheme.colorScheme.surface,
100     contentColor: Color = contentColorFor(color),
101     tonalElevation: Dp = 0.dp,
102     shadowElevation: Dp = 0.dp,
103     border: BorderStroke? = null,
104     content: @Composable () -> Unit
105 ) {
106     val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
107     CompositionLocalProvider(
108         LocalContentColor provides contentColor,
109         LocalAbsoluteTonalElevation provides absoluteElevation
110     ) {
111         Box(
112             modifier =
113                 modifier
114                     .surface(
115                         shape = shape,
116                         backgroundColor =
117                             surfaceColorAtElevation(color = color, elevation = absoluteElevation),
118                         border = border,
119                         shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() }
120                     )
121                     .semantics(mergeDescendants = false) {
122                         // TODO(b/347038246): replace `isContainer` with `isTraversalGroup` with new
123                         // pruning API.
124                         @Suppress("DEPRECATION")
125                         isContainer = true
126                     }
127                     .pointerInput(Unit) {},
128             propagateMinConstraints = true
129         ) {
130             content()
131         }
132     }
133 }
134 
135 /**
136  * Material surface is the central metaphor in material design. Each surface exists at a given
137  * elevation, which influences how that piece of surface visually relates to other surfaces and how
138  * that surface is modified by tonal variance.
139  *
140  * This version of Surface is responsible for a click handling as well as everything else that a
141  * regular Surface does:
142  *
143  * This clickable Surface is responsible for:
144  * 1) Clipping: Surface clips its children to the shape specified by [shape]
145  * 2) Borders: If [shape] has a border, then it will also be drawn.
146  * 3) Background: Surface fills the shape specified by [shape] with the [color]. If [color] is
147  *    [ColorScheme.surface] a color overlay may be applied. The color of the overlay depends on the
148  *    [tonalElevation] of this Surface, and the [LocalAbsoluteTonalElevation] set by any parent
149  *    surfaces. This ensures that a Surface never appears to have a lower elevation overlay than its
150  *    ancestors, by summing the elevation of all previous Surfaces.
151  * 4) Content color: Surface uses [contentColor] to specify a preferred color for the content of
152  *    this surface - this is used by the [Text] and [Icon] components as a default color. If no
153  *    [contentColor] is set, this surface will try and match its background color to a color defined
154  *    in the theme [ColorScheme], and return the corresponding content color. For example, if the
155  *    [color] of this surface is [ColorScheme.surface], [contentColor] will be set to
156  *    [ColorScheme.onSurface]. If [color] is not part of the theme palette, [contentColor] will keep
157  *    the same value set above this Surface.
158  * 5) Click handling. This version of surface will react to the clicks, calling [onClick] lambda,
159  *    updating the [interactionSource] when [PressInteraction] occurs, and showing ripple indication
160  *    in response to press events. If you don't need click handling, consider using the Surface
161  *    function that doesn't require [onClick] param. If you need to set a custom label for the
162  *    [onClick], apply a `Modifier.semantics { onClick(label = "YOUR_LABEL", action = null) }` to
163  *    the Surface.
164  * 6) Semantics for clicks. Just like with [Modifier.clickable], clickable version of Surface will
165  *    produce semantics to indicate that it is clicked. No semantic role is set by default, you may
166  *    specify one by passing a desired [Role] with a [Modifier.semantics].
167  *
168  * To manually retrieve the content color inside a surface, use [LocalContentColor].
169  *
170  * Clickable surface sample:
171  *
172  * @sample androidx.compose.material3.samples.ClickableSurfaceSample
173  * @param onClick callback to be called when the surface is clicked
174  * @param modifier Modifier to be applied to the layout corresponding to the surface
175  * @param enabled Controls the enabled state of the surface. When `false`, this surface will not be
176  *   clickable
177  * @param shape Defines the surface's shape as well its shadow. A shadow is only displayed if the
178  *   [tonalElevation] is greater than zero.
179  * @param color The background color. Use [Color.Transparent] to have no color.
180  * @param contentColor The preferred content color provided by this Surface to its children.
181  *   Defaults to either the matching content color for [color], or if [color] is not a color from
182  *   the theme, this will keep the same value set above this Surface.
183  * @param border Optional border to draw on top of the surface
184  * @param tonalElevation When [color] is [ColorScheme.surface], a higher the elevation will result
185  *   in a darker color in light theme and lighter color in dark theme.
186  * @param shadowElevation The size of the shadow below the surface. Note that It will not affect z
187  *   index of the Surface. If you want to change the drawing order you can use `Modifier.zIndex`.
188  * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
189  *   emitting [Interaction]s for this surface. You can use this to change the surface's appearance
190  *   or preview the surface in different states. Note that if `null` is provided, interactions will
191  *   still happen internally.
192  * @param content The content to be displayed on this Surface
193  */
194 @Composable
195 @NonRestartableComposable
Surfacenull196 fun Surface(
197     onClick: () -> Unit,
198     modifier: Modifier = Modifier,
199     enabled: Boolean = true,
200     shape: Shape = RectangleShape,
201     color: Color = MaterialTheme.colorScheme.surface,
202     contentColor: Color = contentColorFor(color),
203     tonalElevation: Dp = 0.dp,
204     shadowElevation: Dp = 0.dp,
205     border: BorderStroke? = null,
206     interactionSource: MutableInteractionSource? = null,
207     content: @Composable () -> Unit
208 ) {
209     @Suppress("NAME_SHADOWING")
210     val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
211     val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
212     CompositionLocalProvider(
213         LocalContentColor provides contentColor,
214         LocalAbsoluteTonalElevation provides absoluteElevation
215     ) {
216         Box(
217             modifier =
218                 modifier
219                     .minimumInteractiveComponentSize()
220                     .surface(
221                         shape = shape,
222                         backgroundColor =
223                             surfaceColorAtElevation(color = color, elevation = absoluteElevation),
224                         border = border,
225                         shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() }
226                     )
227                     .clickable(
228                         interactionSource = interactionSource,
229                         indication = ripple(),
230                         enabled = enabled,
231                         onClick = onClick
232                     )
233                     .childSemantics(),
234             propagateMinConstraints = true
235         ) {
236             content()
237         }
238     }
239 }
240 
241 /**
242  * Material surface is the central metaphor in material design. Each surface exists at a given
243  * elevation, which influences how that piece of surface visually relates to other surfaces and how
244  * that surface is modified by tonal variance.
245  *
246  * This version of Surface is responsible for a selection handling as well as everything else that a
247  * regular Surface does:
248  *
249  * This selectable Surface is responsible for:
250  * 1) Clipping: Surface clips its children to the shape specified by [shape]
251  * 2) Borders: If [shape] has a border, then it will also be drawn.
252  * 3) Background: Surface fills the shape specified by [shape] with the [color]. If [color] is
253  *    [ColorScheme.surface] a color overlay may be applied. The color of the overlay depends on the
254  *    [tonalElevation] of this Surface, and the [LocalAbsoluteTonalElevation] set by any parent
255  *    surfaces. This ensures that a Surface never appears to have a lower elevation overlay than its
256  *    ancestors, by summing the elevation of all previous Surfaces.
257  * 4) Content color: Surface uses [contentColor] to specify a preferred color for the content of
258  *    this surface - this is used by the [Text] and [Icon] components as a default color. If no
259  *    [contentColor] is set, this surface will try and match its background color to a color defined
260  *    in the theme [ColorScheme], and return the corresponding content color. For example, if the
261  *    [color] of this surface is [ColorScheme.surface], [contentColor] will be set to
262  *    [ColorScheme.onSurface]. If [color] is not part of the theme palette, [contentColor] will keep
263  *    the same value set above this Surface.
264  * 5) Click handling. This version of surface will react to the clicks, calling [onClick] lambda,
265  *    updating the [interactionSource] when [PressInteraction] occurs, and showing ripple indication
266  *    in response to press events. If you don't need click handling, consider using the Surface
267  *    function that doesn't require [onClick] param.
268  * 6) Semantics for selection. Just like with [Modifier.selectable], selectable version of Surface
269  *    will produce semantics to indicate that it is selected. No semantic role is set by default,
270  *    you may specify one by passing a desired [Role] with a [Modifier.semantics].
271  *
272  * To manually retrieve the content color inside a surface, use [LocalContentColor].
273  *
274  * Selectable surface sample:
275  *
276  * @sample androidx.compose.material3.samples.SelectableSurfaceSample
277  * @param selected whether or not this Surface is selected
278  * @param onClick callback to be called when the surface is clicked
279  * @param modifier Modifier to be applied to the layout corresponding to the surface
280  * @param enabled Controls the enabled state of the surface. When `false`, this surface will not be
281  *   clickable
282  * @param shape Defines the surface's shape as well its shadow. A shadow is only displayed if the
283  *   [tonalElevation] is greater than zero.
284  * @param color The background color. Use [Color.Transparent] to have no color.
285  * @param contentColor The preferred content color provided by this Surface to its children.
286  *   Defaults to either the matching content color for [color], or if [color] is not a color from
287  *   the theme, this will keep the same value set above this Surface.
288  * @param border Optional border to draw on top of the surface
289  * @param tonalElevation When [color] is [ColorScheme.surface], a higher the elevation will result
290  *   in a darker color in light theme and lighter color in dark theme.
291  * @param shadowElevation The size of the shadow below the surface. Note that It will not affect z
292  *   index of the Surface. If you want to change the drawing order you can use `Modifier.zIndex`.
293  * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
294  *   emitting [Interaction]s for this surface. You can use this to change the surface's appearance
295  *   or preview the surface in different states. Note that if `null` is provided, interactions will
296  *   still happen internally.
297  * @param content The content to be displayed on this Surface
298  */
299 @Composable
300 @NonRestartableComposable
Surfacenull301 fun Surface(
302     selected: Boolean,
303     onClick: () -> Unit,
304     modifier: Modifier = Modifier,
305     enabled: Boolean = true,
306     shape: Shape = RectangleShape,
307     color: Color = MaterialTheme.colorScheme.surface,
308     contentColor: Color = contentColorFor(color),
309     tonalElevation: Dp = 0.dp,
310     shadowElevation: Dp = 0.dp,
311     border: BorderStroke? = null,
312     interactionSource: MutableInteractionSource? = null,
313     content: @Composable () -> Unit
314 ) {
315     @Suppress("NAME_SHADOWING")
316     val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
317     val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
318     CompositionLocalProvider(
319         LocalContentColor provides contentColor,
320         LocalAbsoluteTonalElevation provides absoluteElevation
321     ) {
322         Box(
323             modifier =
324                 modifier
325                     .minimumInteractiveComponentSize()
326                     .surface(
327                         shape = shape,
328                         backgroundColor =
329                             surfaceColorAtElevation(color = color, elevation = absoluteElevation),
330                         border = border,
331                         shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() }
332                     )
333                     .selectable(
334                         selected = selected,
335                         interactionSource = interactionSource,
336                         indication = ripple(),
337                         enabled = enabled,
338                         onClick = onClick
339                     )
340                     .childSemantics(),
341             propagateMinConstraints = true
342         ) {
343             content()
344         }
345     }
346 }
347 
348 /**
349  * Material surface is the central metaphor in material design. Each surface exists at a given
350  * elevation, which influences how that piece of surface visually relates to other surfaces and how
351  * that surface is modified by tonal variance.
352  *
353  * This version of Surface is responsible for a toggling its checked state as well as everything
354  * else that a regular Surface does:
355  *
356  * This toggleable Surface is responsible for:
357  * 1) Clipping: Surface clips its children to the shape specified by [shape]
358  * 2) Borders: If [shape] has a border, then it will also be drawn.
359  * 3) Background: Surface fills the shape specified by [shape] with the [color]. If [color] is
360  *    [ColorScheme.surface] a color overlay may be applied. The color of the overlay depends on the
361  *    [tonalElevation] of this Surface, and the [LocalAbsoluteTonalElevation] set by any parent
362  *    surfaces. This ensures that a Surface never appears to have a lower elevation overlay than its
363  *    ancestors, by summing the elevation of all previous Surfaces.
364  * 4) Content color: Surface uses [contentColor] to specify a preferred color for the content of
365  *    this surface - this is used by the [Text] and [Icon] components as a default color. If no
366  *    [contentColor] is set, this surface will try and match its background color to a color defined
367  *    in the theme [ColorScheme], and return the corresponding content color. For example, if the
368  *    [color] of this surface is [ColorScheme.surface], [contentColor] will be set to
369  *    [ColorScheme.onSurface]. If [color] is not part of the theme palette, [contentColor] will keep
370  *    the same value set above this Surface.
371  * 5) Click handling. This version of surface will react to the check toggles, calling
372  *    [onCheckedChange] lambda, updating the [interactionSource] when [PressInteraction] occurs, and
373  *    showing ripple indication in response to press events. If you don't need check handling,
374  *    consider using a Surface function that doesn't require [onCheckedChange] param.
375  * 6) Semantics for toggle. Just like with [Modifier.toggleable], toggleable version of Surface will
376  *    produce semantics to indicate that it is checked. No semantic role is set by default, you may
377  *    specify one by passing a desired [Role] with a [Modifier.semantics].
378  *
379  * To manually retrieve the content color inside a surface, use [LocalContentColor].
380  *
381  * Toggleable surface sample:
382  *
383  * @sample androidx.compose.material3.samples.ToggleableSurfaceSample
384  * @param checked whether or not this Surface is toggled on or off
385  * @param onCheckedChange callback to be invoked when the toggleable Surface is clicked
386  * @param modifier Modifier to be applied to the layout corresponding to the surface
387  * @param enabled Controls the enabled state of the surface. When `false`, this surface will not be
388  *   clickable
389  * @param shape Defines the surface's shape as well its shadow. A shadow is only displayed if the
390  *   [tonalElevation] is greater than zero.
391  * @param color The background color. Use [Color.Transparent] to have no color.
392  * @param contentColor The preferred content color provided by this Surface to its children.
393  *   Defaults to either the matching content color for [color], or if [color] is not a color from
394  *   the theme, this will keep the same value set above this Surface.
395  * @param border Optional border to draw on top of the surface
396  * @param tonalElevation When [color] is [ColorScheme.surface], a higher the elevation will result
397  *   in a darker color in light theme and lighter color in dark theme.
398  * @param shadowElevation The size of the shadow below the surface. Note that It will not affect z
399  *   index of the Surface. If you want to change the drawing order you can use `Modifier.zIndex`.
400  * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
401  *   emitting [Interaction]s for this surface. You can use this to change the surface's appearance
402  *   or preview the surface in different states. Note that if `null` is provided, interactions will
403  *   still happen internally.
404  * @param content The content to be displayed on this Surface
405  */
406 @Composable
407 @NonRestartableComposable
Surfacenull408 fun Surface(
409     checked: Boolean,
410     onCheckedChange: (Boolean) -> Unit,
411     modifier: Modifier = Modifier,
412     enabled: Boolean = true,
413     shape: Shape = RectangleShape,
414     color: Color = MaterialTheme.colorScheme.surface,
415     contentColor: Color = contentColorFor(color),
416     tonalElevation: Dp = 0.dp,
417     shadowElevation: Dp = 0.dp,
418     border: BorderStroke? = null,
419     interactionSource: MutableInteractionSource? = null,
420     content: @Composable () -> Unit
421 ) {
422     @Suppress("NAME_SHADOWING")
423     val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
424     val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
425     CompositionLocalProvider(
426         LocalContentColor provides contentColor,
427         LocalAbsoluteTonalElevation provides absoluteElevation
428     ) {
429         Box(
430             modifier =
431                 modifier
432                     .minimumInteractiveComponentSize()
433                     .surface(
434                         shape = shape,
435                         backgroundColor =
436                             surfaceColorAtElevation(color = color, elevation = absoluteElevation),
437                         border = border,
438                         shadowElevation = with(LocalDensity.current) { shadowElevation.toPx() }
439                     )
440                     .toggleable(
441                         value = checked,
442                         interactionSource = interactionSource,
443                         indication = ripple(),
444                         enabled = enabled,
445                         onValueChange = onCheckedChange
446                     )
447                     .childSemantics(),
448             propagateMinConstraints = true
449         ) {
450             content()
451         }
452     }
453 }
454 
455 @Stable
surfacenull456 private fun Modifier.surface(
457     shape: Shape,
458     backgroundColor: Color,
459     border: BorderStroke?,
460     shadowElevation: Float,
461 ) =
462     this.then(
463             if (shadowElevation > 0f) {
464                 Modifier.graphicsLayer(
465                     shadowElevation = shadowElevation,
466                     shape = shape,
467                     clip = false
468                 )
469             } else {
470                 Modifier
471             }
472         )
473         .then(if (border != null) Modifier.border(border, shape) else Modifier)
474         .background(color = backgroundColor, shape = shape)
475         .clip(shape)
476 
477 @Composable
surfaceColorAtElevationnull478 private fun surfaceColorAtElevation(color: Color, elevation: Dp): Color =
479     MaterialTheme.colorScheme.applyTonalElevation(color, elevation)
480 
481 /**
482  * CompositionLocal containing the current absolute elevation provided by [Surface] components. This
483  * absolute elevation is a sum of all the previous elevations. Absolute elevation is only used for
484  * calculating surface tonal colors, and is *not* used for drawing the shadow in a [Surface].
485  */
486 // TODO(b/179787782): Add sample after catalog app lands in aosp.
487 val LocalAbsoluteTonalElevation = compositionLocalOf { 0.dp }
488