1 /*
2  * Copyright 2022 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.foundation.interaction.Interaction
20 import androidx.compose.foundation.interaction.MutableInteractionSource
21 import androidx.compose.foundation.layout.Arrangement
22 import androidx.compose.foundation.layout.Row
23 import androidx.compose.foundation.layout.RowScope
24 import androidx.compose.runtime.Composable
25 import androidx.compose.ui.Alignment
26 import androidx.compose.ui.Modifier
27 import androidx.compose.ui.focus.onFocusChanged
28 import androidx.compose.ui.graphics.Color
29 import androidx.compose.ui.graphics.RectangleShape
30 import androidx.compose.ui.semantics.Role
31 import androidx.compose.ui.semantics.role
32 import androidx.compose.ui.semantics.selected
33 import androidx.compose.ui.semantics.semantics
34 
35 /**
36  * Material Design tab.
37  *
38  * A default Tab, also known as a Primary Navigation Tab. Tabs organize content across different
39  * screens, data sets, and other interactions.
40  *
41  * This should typically be used inside of a [TabRow], see the corresponding documentation for
42  * example usage.
43  *
44  * @param selected whether this tab is selected or not
45  * @param onFocus called when this tab is focused
46  * @param modifier the [Modifier] to be applied to this tab
47  * @param onClick called when this tab is clicked (with D-Pad Center)
48  * @param enabled controls the enabled state of this tab. When `false`, this component will not
49  *   respond to user input, and it will appear visually disabled and disabled to accessibility
50  *   services.
51  * @param colors these will be used by the tab when in different states (focused, selected, etc.)
52  * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
53  *   emitting [Interaction]s for this tab. You can use this to change the tab's appearance or
54  *   preview the tab in different states. Note that if `null` is provided, interactions will still
55  *   happen internally.
56  * @param content content of the [Tab]
57  */
58 @Composable
Tabnull59 fun TabRowScope.Tab(
60     selected: Boolean,
61     onFocus: () -> Unit,
62     modifier: Modifier = Modifier,
63     onClick: () -> Unit = {},
64     enabled: Boolean = true,
65     colors: TabColors = TabDefaults.pillIndicatorTabColors(),
66     interactionSource: MutableInteractionSource? = null,
67     content: @Composable RowScope.() -> Unit
68 ) {
69     Surface(
70         selected = selected,
71         onClick = onClick,
72         modifier =
73             modifier
<lambda>null74                 .onFocusChanged {
75                     if (it.isFocused) {
76                         onFocus()
77                     }
78                 }
<lambda>null79                 .semantics {
80                     this.selected = selected
81                     this.role = Role.Tab
82                 },
83         colors =
84             colors.toSelectableSurfaceColors(
85                 doesTabRowHaveFocus = hasFocus,
86                 enabled = enabled,
87             ),
88         enabled = enabled,
89         scale = SelectableSurfaceScale.None,
90         shape = SelectableSurfaceDefaults.shape(shape = RectangleShape),
91         interactionSource = interactionSource,
<lambda>null92     ) {
93         Row(
94             horizontalArrangement = Arrangement.Center,
95             verticalAlignment = Alignment.CenterVertically,
96             content = content
97         )
98     }
99 }
100 
101 /**
102  * Represents the colors used in a tab in different states.
103  * - See [TabDefaults.pillIndicatorTabColors] for the default colors used in a [Tab] when using a
104  *   Pill indicator.
105  * - See [TabDefaults.underlinedIndicatorTabColors] for the default colors used in a [Tab] when
106  *   using an Underlined indicator
107  */
108 class TabColors
109 internal constructor(
110     internal val contentColor: Color,
111     internal val inactiveContentColor: Color = contentColor.copy(alpha = 0.4f),
112     internal val selectedContentColor: Color,
113     internal val focusedContentColor: Color,
114     internal val focusedSelectedContentColor: Color,
115     internal val disabledContentColor: Color,
116     internal val disabledInactiveContentColor: Color = disabledContentColor.copy(alpha = 0.4f),
117     internal val disabledSelectedContentColor: Color,
118 ) {
equalsnull119     override fun equals(other: Any?): Boolean {
120         if (this === other) return true
121         if (other == null || other !is TabColors) return false
122 
123         if (contentColor != other.contentColor) return false
124         if (inactiveContentColor != other.inactiveContentColor) return false
125         if (selectedContentColor != other.selectedContentColor) return false
126         if (focusedContentColor != other.focusedContentColor) return false
127         if (focusedSelectedContentColor != other.focusedSelectedContentColor) return false
128         if (disabledContentColor != other.disabledContentColor) return false
129         if (disabledInactiveContentColor != other.disabledInactiveContentColor) return false
130         if (disabledSelectedContentColor != other.disabledSelectedContentColor) return false
131 
132         return true
133     }
134 
hashCodenull135     override fun hashCode(): Int {
136         var result = contentColor.hashCode()
137         result = 31 * result + inactiveContentColor.hashCode()
138         result = 31 * result + selectedContentColor.hashCode()
139         result = 31 * result + focusedContentColor.hashCode()
140         result = 31 * result + focusedSelectedContentColor.hashCode()
141         result = 31 * result + disabledContentColor.hashCode()
142         result = 31 * result + disabledInactiveContentColor.hashCode()
143         result = 31 * result + disabledSelectedContentColor.hashCode()
144         return result
145     }
146 }
147 
148 object TabDefaults {
149     /**
150      * [Tab]'s content colors to in conjunction with underlined indicator
151      *
152      * @param contentColor applied when the any of the other tabs is focused
153      * @param inactiveContentColor the default color of the tab's content when none of the tabs are
154      *   focused
155      * @param selectedContentColor applied when the current tab is selected
156      * @param focusedContentColor applied when the current tab is focused
157      * @param focusedSelectedContentColor applied when the current tab is both focused and selected
158      * @param disabledContentColor applied when any of the other tabs is focused and the current tab
159      *   is disabled
160      * @param disabledInactiveContentColor applied when the current tab is disabled and none of the
161      *   tabs are focused
162      * @param disabledSelectedContentColor applied when the current tab is disabled and selected
163      */
164     @Composable
underlinedIndicatorTabColorsnull165     fun underlinedIndicatorTabColors(
166         contentColor: Color = LocalContentColor.current,
167         inactiveContentColor: Color = contentColor.copy(alpha = 0.4f),
168         selectedContentColor: Color = MaterialTheme.colorScheme.onPrimaryContainer,
169         focusedContentColor: Color = MaterialTheme.colorScheme.primary,
170         focusedSelectedContentColor: Color = focusedContentColor,
171         disabledContentColor: Color = contentColor,
172         disabledInactiveContentColor: Color = disabledContentColor.copy(alpha = 0.4f),
173         disabledSelectedContentColor: Color = selectedContentColor,
174     ): TabColors =
175         TabColors(
176             contentColor = contentColor,
177             inactiveContentColor = inactiveContentColor,
178             selectedContentColor = selectedContentColor,
179             focusedContentColor = focusedContentColor,
180             focusedSelectedContentColor = focusedSelectedContentColor,
181             disabledContentColor = disabledContentColor,
182             disabledInactiveContentColor = disabledInactiveContentColor,
183             disabledSelectedContentColor = disabledSelectedContentColor,
184         )
185 
186     /**
187      * [Tab]'s content colors to in conjunction with pill indicator
188      *
189      * @param contentColor applied when the any of the other tabs is focused
190      * @param inactiveContentColor the default color of the tab's content when none of the tabs are
191      *   focused
192      * @param selectedContentColor applied when the current tab is selected
193      * @param focusedContentColor applied when the current tab is focused
194      * @param focusedSelectedContentColor applied when the current tab is both focused and selected
195      * @param disabledContentColor applied when any of the other tabs is focused and the current tab
196      *   is disabled
197      * @param disabledInactiveContentColor applied when the current tab is disabled and none of the
198      *   tabs are focused
199      * @param disabledSelectedContentColor applied when the current tab is disabled and selected
200      */
201     @Composable
202     fun pillIndicatorTabColors(
203         contentColor: Color = LocalContentColor.current,
204         inactiveContentColor: Color = contentColor.copy(alpha = 0.4f),
205         selectedContentColor: Color = MaterialTheme.colorScheme.onPrimaryContainer,
206         focusedContentColor: Color = MaterialTheme.colorScheme.surfaceVariant,
207         focusedSelectedContentColor: Color = focusedContentColor,
208         disabledContentColor: Color = contentColor,
209         disabledInactiveContentColor: Color = disabledContentColor.copy(alpha = 0.4f),
210         disabledSelectedContentColor: Color = selectedContentColor,
211     ): TabColors =
212         TabColors(
213             contentColor = contentColor,
214             inactiveContentColor = inactiveContentColor,
215             selectedContentColor = selectedContentColor,
216             focusedContentColor = focusedContentColor,
217             focusedSelectedContentColor = focusedSelectedContentColor,
218             disabledContentColor = disabledContentColor,
219             disabledInactiveContentColor = disabledInactiveContentColor,
220             disabledSelectedContentColor = disabledSelectedContentColor,
221         )
222 }
223 
224 @Composable
225 internal fun TabColors.toSelectableSurfaceColors(
226     doesTabRowHaveFocus: Boolean,
227     enabled: Boolean,
228 ) =
229     SelectableSurfaceDefaults.colors(
230         contentColor = if (doesTabRowHaveFocus) contentColor else inactiveContentColor,
231         selectedContentColor = if (enabled) selectedContentColor else disabledSelectedContentColor,
232         focusedContentColor = focusedContentColor,
233         focusedSelectedContentColor = focusedSelectedContentColor,
234         disabledContentColor =
235             if (doesTabRowHaveFocus) disabledContentColor else disabledInactiveContentColor,
236         containerColor = Color.Transparent,
237         focusedContainerColor = Color.Transparent,
238         pressedContainerColor = Color.Transparent,
239         focusedSelectedContainerColor = Color.Transparent,
240         selectedContainerColor = Color.Transparent,
241         pressedSelectedContainerColor = Color.Transparent,
242         disabledContainerColor = Color.Transparent,
243     )
244