1 /*
<lambda>null2  * Copyright 2025 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.tv.material3
18 
19 import androidx.compose.animation.core.animateFloatAsState
20 import androidx.compose.foundation.background
21 import androidx.compose.foundation.interaction.MutableInteractionSource
22 import androidx.compose.foundation.interaction.PressInteraction
23 import androidx.compose.foundation.interaction.collectIsFocusedAsState
24 import androidx.compose.foundation.interaction.collectIsPressedAsState
25 import androidx.compose.foundation.layout.Box
26 import androidx.compose.foundation.layout.BoxScope
27 import androidx.compose.runtime.Composable
28 import androidx.compose.runtime.CompositionLocalProvider
29 import androidx.compose.runtime.compositionLocalOf
30 import androidx.compose.runtime.getValue
31 import androidx.compose.runtime.mutableStateOf
32 import androidx.compose.runtime.remember
33 import androidx.compose.runtime.rememberCoroutineScope
34 import androidx.compose.runtime.setValue
35 import androidx.compose.ui.Modifier
36 import androidx.compose.ui.composed
37 import androidx.compose.ui.focus.onFocusChanged
38 import androidx.compose.ui.geometry.Offset
39 import androidx.compose.ui.graphics.Color
40 import androidx.compose.ui.graphics.Shape
41 import androidx.compose.ui.graphics.graphicsLayer
42 import androidx.compose.ui.input.key.NativeKeyEvent
43 import androidx.compose.ui.input.key.onKeyEvent
44 import androidx.compose.ui.platform.debugInspectorInfo
45 import androidx.compose.ui.unit.Dp
46 import androidx.compose.ui.unit.dp
47 import androidx.compose.ui.zIndex
48 import kotlinx.coroutines.launch
49 
50 @Composable
51 internal fun SurfaceImpl(
52     modifier: Modifier,
53     selected: Boolean,
54     enabled: Boolean,
55     shape: Shape,
56     color: Color,
57     contentColor: Color,
58     scale: Float,
59     border: Border,
60     glow: Glow,
61     tonalElevation: Dp,
62     interactionSource: MutableInteractionSource? = null,
63     content: @Composable (BoxScope.() -> Unit)
64 ) {
65     @Suppress("NAME_SHADOWING")
66     val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
67     val focused by interactionSource.collectIsFocusedAsState()
68     val pressed by interactionSource.collectIsPressedAsState()
69 
70     val surfaceAlpha =
71         calculateAlphaBasedOnState(
72             enabled = enabled,
73             focused = focused,
74             pressed = pressed,
75             selected = selected
76         )
77 
78     val absoluteElevation = LocalAbsoluteTonalElevation.current + tonalElevation
79 
80     CompositionLocalProvider(
81         LocalContentColor provides contentColor,
82         LocalAbsoluteTonalElevation provides absoluteElevation
83     ) {
84         val zIndex by
85             animateFloatAsState(
86                 targetValue = if (focused) FocusedZIndex else NonFocusedZIndex,
87                 label = "zIndex"
88             )
89 
90         val backgroundColorByState =
91             calculateSurfaceColorAtElevation(
92                 color = color,
93                 elevation = LocalAbsoluteTonalElevation.current
94             )
95 
96         Box(
97             modifier =
98                 modifier
99                     .tvSurfaceScale(
100                         scale = scale,
101                         interactionSource = interactionSource,
102                     )
103                     .ifElse(API_28_OR_ABOVE, Modifier.tvSurfaceGlow(shape, glow))
104                     // Increasing the zIndex of this Surface when it is in the focused state to
105                     // avoid the glowIndication from being overlapped by subsequent items if
106                     // this Surface is inside a list composable (like a Row/Column).
107                     .zIndex(zIndex)
108                     .ifElse(border != Border.None, Modifier.tvSurfaceBorder(shape, border))
109                     .background(backgroundColorByState, shape)
110                     .graphicsLayer {
111                         this.alpha = surfaceAlpha
112                         this.shape = shape
113                         this.clip = true
114                     },
115             propagateMinConstraints = true
116         ) {
117             Box(
118                 modifier =
119                     Modifier.graphicsLayer {
120                         this.alpha = if (!enabled) DisabledContentAlpha else EnabledContentAlpha
121                     },
122                 content = content
123             )
124         }
125     }
126 }
127 
handleDPadEnternull128 internal fun Modifier.handleDPadEnter(
129     enabled: Boolean,
130     interactionSource: MutableInteractionSource,
131     onClick: (() -> Unit)? = null,
132     onLongClick: (() -> Unit)? = null,
133     selected: Boolean = false,
134 ) =
135     composed(
136         inspectorInfo =
137             debugInspectorInfo {
138                 name = "handleDPadEnter"
139                 properties["enabled"] = enabled
140                 properties["interactionSource"] = interactionSource
141                 properties["onClick"] = onClick
142                 properties["onLongClick"] = onLongClick
143                 properties["selected"] = selected
144             }
<lambda>null145     ) {
146         if (!enabled) return@composed this
147 
148         val coroutineScope = rememberCoroutineScope()
149         val pressInteraction = remember { PressInteraction.Press(Offset.Zero) }
150         var isLongClick by remember { mutableStateOf(false) }
151         val isPressed by interactionSource.collectIsPressedAsState()
152 
153         this.onFocusChanged {
154                 if (!it.isFocused && isPressed) {
155                     coroutineScope.launch {
156                         interactionSource.emit(PressInteraction.Release(pressInteraction))
157                     }
158                 }
159             }
160             .onKeyEvent { keyEvent ->
161                 if (AcceptableEnterClickKeys.contains(keyEvent.nativeKeyEvent.keyCode)) {
162                     when (keyEvent.nativeKeyEvent.action) {
163                         NativeKeyEvent.ACTION_DOWN -> {
164                             when (keyEvent.nativeKeyEvent.repeatCount) {
165                                 0 ->
166                                     coroutineScope.launch {
167                                         interactionSource.emit(pressInteraction)
168                                     }
169                                 1 ->
170                                     onLongClick?.let {
171                                         isLongClick = true
172                                         coroutineScope.launch {
173                                             interactionSource.emit(
174                                                 PressInteraction.Release(pressInteraction)
175                                             )
176                                         }
177                                         it.invoke()
178                                     }
179                             }
180                         }
181                         NativeKeyEvent.ACTION_UP -> {
182                             if (!isLongClick) {
183                                 coroutineScope.launch {
184                                     interactionSource.emit(
185                                         PressInteraction.Release(pressInteraction)
186                                     )
187                                 }
188                                 onClick?.invoke()
189                             } else isLongClick = false
190                         }
191                     }
192                     return@onKeyEvent KeyEventPropagation.StopPropagation
193                 }
194                 KeyEventPropagation.ContinuePropagation
195             }
196     }
197 
calculateAlphaBasedOnStatenull198 private fun calculateAlphaBasedOnState(
199     enabled: Boolean,
200     focused: Boolean,
201     pressed: Boolean,
202     selected: Boolean
203 ): Float {
204     return when {
205         !enabled && pressed -> DisabledPressedStateAlpha
206         !enabled && focused -> DisabledFocusedStateAlpha
207         !enabled && selected -> DisabledSelectedStateAlpha
208         enabled -> EnabledContentAlpha
209         else -> DisabledDefaultStateAlpha
210     }
211 }
212 
213 @Composable
calculateSurfaceColorAtElevationnull214 internal fun calculateSurfaceColorAtElevation(color: Color, elevation: Dp): Color {
215     return if (color == MaterialTheme.colorScheme.surface) {
216         MaterialTheme.colorScheme.surfaceColorAtElevation(elevation)
217     } else {
218         color
219     }
220 }
221 
222 private const val DisabledPressedStateAlpha = 0.8f
223 private const val DisabledFocusedStateAlpha = 0.8f
224 private const val DisabledSelectedStateAlpha = 0.8f
225 private const val DisabledDefaultStateAlpha = 0.6f
226 
227 private const val FocusedZIndex = 0.5f
228 private const val NonFocusedZIndex = 0f
229 
230 private const val DisabledContentAlpha = 0.8f
231 private const val EnabledContentAlpha = 1f
232 
233 private val AcceptableEnterClickKeys =
234     intArrayOf(
235         NativeKeyEvent.KEYCODE_DPAD_CENTER,
236         NativeKeyEvent.KEYCODE_ENTER,
237         NativeKeyEvent.KEYCODE_NUMPAD_ENTER
238     )
239 
240 /**
241  * CompositionLocal containing the current absolute elevation provided by Surface components. This
242  * absolute elevation is a sum of all the previous elevations. Absolute elevation is only used for
243  * calculating surface tonal colors, and is *not* used for drawing the shadow in a [SurfaceImpl].
244  */
<lambda>null245 internal val LocalAbsoluteTonalElevation = compositionLocalOf { 0.dp }
246