1 /*
<lambda>null2  * 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.compose.material3.internal
18 
19 import androidx.compose.animation.animateColor
20 import androidx.compose.animation.animateColorAsState
21 import androidx.compose.animation.core.animateDpAsState
22 import androidx.compose.animation.core.animateFloat
23 import androidx.compose.animation.core.updateTransition
24 import androidx.compose.foundation.BorderStroke
25 import androidx.compose.foundation.interaction.InteractionSource
26 import androidx.compose.foundation.interaction.collectIsFocusedAsState
27 import androidx.compose.foundation.layout.Box
28 import androidx.compose.foundation.layout.PaddingValues
29 import androidx.compose.material3.LocalContentColor
30 import androidx.compose.material3.LocalMinimumInteractiveComponentSize
31 import androidx.compose.material3.MaterialTheme
32 import androidx.compose.material3.OutlinedTextFieldLayout
33 import androidx.compose.material3.TextFieldColors
34 import androidx.compose.material3.TextFieldLabelPosition
35 import androidx.compose.material3.TextFieldLabelScope
36 import androidx.compose.material3.TextFieldLayout
37 import androidx.compose.material3.outlineCutout
38 import androidx.compose.material3.tokens.MotionSchemeKeyTokens
39 import androidx.compose.material3.tokens.SmallIconButtonTokens
40 import androidx.compose.material3.tokens.TypeScaleTokens
41 import androidx.compose.material3.value
42 import androidx.compose.runtime.Composable
43 import androidx.compose.runtime.CompositionLocalProvider
44 import androidx.compose.runtime.State
45 import androidx.compose.runtime.derivedStateOf
46 import androidx.compose.runtime.getValue
47 import androidx.compose.runtime.mutableStateOf
48 import androidx.compose.runtime.remember
49 import androidx.compose.runtime.rememberUpdatedState
50 import androidx.compose.runtime.structuralEqualityPolicy
51 import androidx.compose.ui.Alignment
52 import androidx.compose.ui.Modifier
53 import androidx.compose.ui.draw.drawWithCache
54 import androidx.compose.ui.geometry.Size
55 import androidx.compose.ui.graphics.Color
56 import androidx.compose.ui.graphics.ColorProducer
57 import androidx.compose.ui.graphics.Shape
58 import androidx.compose.ui.graphics.drawOutline
59 import androidx.compose.ui.graphics.graphicsLayer
60 import androidx.compose.ui.graphics.takeOrElse
61 import androidx.compose.ui.layout.layout
62 import androidx.compose.ui.layout.layoutId
63 import androidx.compose.ui.platform.LocalDensity
64 import androidx.compose.ui.semantics.error
65 import androidx.compose.ui.semantics.semantics
66 import androidx.compose.ui.text.TextStyle
67 import androidx.compose.ui.text.lerp
68 import androidx.compose.ui.unit.Dp
69 import androidx.compose.ui.unit.coerceAtLeast
70 import androidx.compose.ui.unit.constrainHeight
71 import androidx.compose.ui.unit.dp
72 import androidx.compose.ui.unit.isUnspecified
73 
74 internal enum class TextFieldType {
75     Filled,
76     Outlined
77 }
78 
79 @Composable
80 internal fun CommonDecorationBox(
81     type: TextFieldType,
82     visualText: CharSequence,
83     innerTextField: @Composable () -> Unit,
84     labelPosition: TextFieldLabelPosition,
85     label: @Composable (TextFieldLabelScope.() -> Unit)?,
86     placeholder: @Composable (() -> Unit)?,
87     leadingIcon: @Composable (() -> Unit)?,
88     trailingIcon: @Composable (() -> Unit)?,
89     prefix: @Composable (() -> Unit)?,
90     suffix: @Composable (() -> Unit)?,
91     supportingText: @Composable (() -> Unit)?,
92     singleLine: Boolean,
93     enabled: Boolean,
94     isError: Boolean,
95     interactionSource: InteractionSource,
96     contentPadding: PaddingValues,
97     colors: TextFieldColors,
98     container: @Composable () -> Unit,
99 ) {
100     val isFocused = interactionSource.collectIsFocusedAsState().value
101     val inputState =
102         when {
103             isFocused -> InputPhase.Focused
104             visualText.isEmpty() -> InputPhase.UnfocusedEmpty
105             else -> InputPhase.UnfocusedNotEmpty
106         }
107 
108     val labelColor = colors.labelColor(enabled, isError, isFocused)
109 
110     val typography = MaterialTheme.typography
111     val bodyLarge = typography.bodyLarge
112     val bodySmall = typography.bodySmall
113     val overrideLabelTextStyleColor =
114         (bodyLarge.color == Color.Unspecified && bodySmall.color != Color.Unspecified) ||
115             (bodyLarge.color != Color.Unspecified && bodySmall.color == Color.Unspecified)
116 
117     TextFieldTransitionScope(
118         inputState = inputState,
119         focusedLabelTextStyleColor =
120             with(bodySmall.color) {
<lambda>null121                 if (overrideLabelTextStyleColor) this.takeOrElse { labelColor } else this
122             },
123         unfocusedLabelTextStyleColor =
124             with(bodyLarge.color) {
<lambda>null125                 if (overrideLabelTextStyleColor) this.takeOrElse { labelColor } else this
126             },
127         labelColor = labelColor,
128         showExpandedLabel = label != null && labelPosition.showExpandedLabel,
129     ) { labelProgress, labelTextStyleColor, labelContentColor, placeholderAlpha, prefixSuffixAlpha
130         ->
<lambda>null131         val labelScope = remember {
132             object : TextFieldLabelScope {
133                 override val labelMinimizedProgress: Float
134                     get() = labelProgress.value
135             }
136         }
137         val decoratedLabel: @Composable (() -> Unit)? =
labelnull138             label?.let { label ->
139                 @Composable {
140                     val labelTextStyle =
141                         lerp(bodyLarge, bodySmall, labelProgress.value).let { textStyle ->
142                             if (overrideLabelTextStyleColor) {
143                                 textStyle.copy(color = labelTextStyleColor.value)
144                             } else {
145                                 textStyle
146                             }
147                         }
148                     Decoration(labelContentColor.value, labelTextStyle) { labelScope.label() }
149                 }
150             }
151 
152         // Transparent components interfere with Talkback (b/261061240), so if any components below
153         // have alpha == 0, we set the component to null instead.
154 
155         val placeholderColor = colors.placeholderColor(enabled, isError, isFocused)
<lambda>null156         val showPlaceholder by remember {
157             derivedStateOf(structuralEqualityPolicy()) { placeholderAlpha.value > 0f }
158         }
159         val decoratedPlaceholder: @Composable ((Modifier) -> Unit)? =
160             if (placeholder != null && visualText.isEmpty() && showPlaceholder) {
modifiernull161                 @Composable { modifier ->
162                     Box(modifier.graphicsLayer { alpha = placeholderAlpha.value }) {
163                         Decoration(
164                             contentColor = placeholderColor,
165                             textStyle = bodyLarge,
166                             content = placeholder
167                         )
168                     }
169                 }
170             } else null
171 
172         val prefixColor = colors.prefixColor(enabled, isError, isFocused)
<lambda>null173         val showPrefixSuffix by remember {
174             derivedStateOf(structuralEqualityPolicy()) { prefixSuffixAlpha.value > 0f }
175         }
176         val decoratedPrefix: @Composable (() -> Unit)? =
177             if (prefix != null && showPrefixSuffix) {
<lambda>null178                 @Composable {
179                     Box(Modifier.graphicsLayer { alpha = prefixSuffixAlpha.value }) {
180                         Decoration(
181                             contentColor = prefixColor,
182                             textStyle = bodyLarge,
183                             content = prefix
184                         )
185                     }
186                 }
187             } else null
188 
189         val suffixColor = colors.suffixColor(enabled, isError, isFocused)
190         val decoratedSuffix: @Composable (() -> Unit)? =
191             if (suffix != null && showPrefixSuffix) {
<lambda>null192                 @Composable {
193                     Box(Modifier.graphicsLayer { alpha = prefixSuffixAlpha.value }) {
194                         Decoration(
195                             contentColor = suffixColor,
196                             textStyle = bodyLarge,
197                             content = suffix
198                         )
199                     }
200                 }
201             } else null
202 
203         val leadingIconColor = colors.leadingIconColor(enabled, isError, isFocused)
204         val decoratedLeading: @Composable (() -> Unit)? =
<lambda>null205             leadingIcon?.let {
206                 @Composable { Decoration(contentColor = leadingIconColor, content = it) }
207             }
208 
209         val trailingIconColor = colors.trailingIconColor(enabled, isError, isFocused)
210         val decoratedTrailing: @Composable (() -> Unit)? =
<lambda>null211             trailingIcon?.let {
212                 @Composable { Decoration(contentColor = trailingIconColor, content = it) }
213             }
214 
215         val supportingTextColor = colors.supportingTextColor(enabled, isError, isFocused)
216         val decoratedSupporting: @Composable (() -> Unit)? =
<lambda>null217             supportingText?.let {
218                 @Composable {
219                     Decoration(
220                         contentColor = supportingTextColor,
221                         textStyle = bodySmall,
222                         content = it
223                     )
224                 }
225             }
226 
227         when (type) {
228             TextFieldType.Filled -> {
229                 val containerWithId: @Composable () -> Unit = {
<lambda>null230                     Box(Modifier.layoutId(ContainerId), propagateMinConstraints = true) {
231                         container()
232                     }
233                 }
234 
235                 TextFieldLayout(
236                     modifier = Modifier,
237                     textField = innerTextField,
238                     placeholder = decoratedPlaceholder,
239                     label = decoratedLabel,
240                     leading = decoratedLeading,
241                     trailing = decoratedTrailing,
242                     prefix = decoratedPrefix,
243                     suffix = decoratedSuffix,
244                     container = containerWithId,
245                     supporting = decoratedSupporting,
246                     singleLine = singleLine,
247                     labelPosition = labelPosition,
248                     labelProgress = labelProgress::value,
249                     paddingValues = contentPadding
250                 )
251             }
252             TextFieldType.Outlined -> {
253                 // Outlined cutout
<lambda>null254                 val cutoutSize = remember { mutableStateOf(Size.Zero) }
255                 val borderContainerWithId: @Composable () -> Unit = {
256                     Box(
257                         Modifier.layoutId(ContainerId)
258                             .outlineCutout(
259                                 labelSize = cutoutSize::value,
260                                 alignment = labelPosition.minimizedAlignment,
261                                 paddingValues = contentPadding
262                             ),
263                         propagateMinConstraints = true
<lambda>null264                     ) {
265                         container()
266                     }
267                 }
268 
269                 OutlinedTextFieldLayout(
270                     modifier = Modifier,
271                     textField = innerTextField,
272                     placeholder = decoratedPlaceholder,
273                     label = decoratedLabel,
274                     leading = decoratedLeading,
275                     trailing = decoratedTrailing,
276                     prefix = decoratedPrefix,
277                     suffix = decoratedSuffix,
278                     supporting = decoratedSupporting,
279                     singleLine = singleLine,
<lambda>null280                     onLabelMeasured = {
281                         if (labelPosition is TextFieldLabelPosition.Above) {
282                             return@OutlinedTextFieldLayout
283                         }
284                         val progress = labelProgress.value
285                         val labelWidth = it.width * progress
286                         val labelHeight = it.height * progress
287                         if (
288                             cutoutSize.value.width != labelWidth ||
289                                 cutoutSize.value.height != labelHeight
290                         ) {
291                             cutoutSize.value = Size(labelWidth, labelHeight)
292                         }
293                     },
294                     labelPosition = labelPosition,
295                     labelProgress = labelProgress::value,
296                     container = borderContainerWithId,
297                     paddingValues = contentPadding
298                 )
299             }
300         }
301     }
302 }
303 
304 private val TextFieldLabelPosition.showExpandedLabel: Boolean
305     get() = this is TextFieldLabelPosition.Attached && !alwaysMinimize
306 
307 internal val TextFieldLabelPosition.minimizedAlignment: Alignment.Horizontal
308     get() =
309         when (this) {
310             is TextFieldLabelPosition.Above -> alignment
311             is TextFieldLabelPosition.Attached -> minimizedAlignment
312             else -> throw IllegalArgumentException("Unknown position: $this")
313         }
314 
315 internal val TextFieldLabelPosition.expandedAlignment: Alignment.Horizontal
316     get() =
317         when (this) {
318             is TextFieldLabelPosition.Above -> alignment
319             is TextFieldLabelPosition.Attached -> expandedAlignment
320             else -> throw IllegalArgumentException("Unknown position: $this")
321         }
322 
323 /** Decorates [content] with [contentColor] and [textStyle]. */
324 @Composable
Decorationnull325 private fun Decoration(contentColor: Color, textStyle: TextStyle, content: @Composable () -> Unit) =
326     ProvideContentColorTextStyle(contentColor, textStyle, content)
327 
328 /** Decorates [content] with [contentColor]. */
329 @Composable
330 private fun Decoration(contentColor: Color, content: @Composable () -> Unit) =
331     CompositionLocalProvider(LocalContentColor provides contentColor, content = content)
332 
333 // Developers need to handle invalid input manually. But since we don't provide an error message
334 // slot API, we can set the default error message in case developers forget about it.
335 internal fun Modifier.defaultErrorSemantics(
336     isError: Boolean,
337     defaultErrorMessage: String,
338 ): Modifier = if (isError) semantics { error(defaultErrorMessage) } else this
339 
340 /**
341  * Replacement for Modifier.background which takes color lazily to avoid recomposition while
342  * animating.
343  */
textFieldBackgroundnull344 internal fun Modifier.textFieldBackground(
345     color: ColorProducer,
346     shape: Shape,
347 ): Modifier =
348     this.drawWithCache {
349         val outline = shape.createOutline(size, layoutDirection, this)
350         onDrawBehind { drawOutline(outline, color = color()) }
351     }
352 
353 /**
354  * Replacement for Modifier.heightIn which takes the constraint lazily to avoid recomposition while
355  * animating.
356  */
textFieldLabelMinHeightnull357 internal fun Modifier.textFieldLabelMinHeight(minHeight: () -> Dp): Modifier =
358     this.layout { measurable, constraints ->
359         @Suppress("NAME_SHADOWING") val minHeight = minHeight()
360         val resolvedMinHeight =
361             constraints.constrainHeight(
362                 if (minHeight != Dp.Unspecified) minHeight.roundToPx() else 0
363             )
364         val placeable = measurable.measure(constraints.copy(minHeight = resolvedMinHeight))
365         layout(placeable.width, placeable.height) { placeable.place(0, 0) }
366     }
367 
368 @Suppress("BanInlineOptIn")
369 @Composable
TextFieldTransitionScopenull370 private inline fun TextFieldTransitionScope(
371     inputState: InputPhase,
372     focusedLabelTextStyleColor: Color,
373     unfocusedLabelTextStyleColor: Color,
374     labelColor: Color,
375     showExpandedLabel: Boolean,
376     content:
377         @Composable
378         (
379             labelProgress: State<Float>,
380             labelTextStyleColor: State<Color>,
381             labelContentColor: State<Color>,
382             placeholderOpacity: State<Float>,
383             prefixSuffixOpacity: State<Float>,
384         ) -> Unit
385 ) {
386     // Transitions from/to InputPhase.Focused are the most critical in the transition below.
387     // UnfocusedEmpty <-> UnfocusedNotEmpty are needed when a single state is used to control
388     // multiple text fields.
389     val transition = updateTransition(inputState, label = "TextFieldInputState")
390 
391     // TODO Load the motionScheme tokens from the component tokens file
392     val labelTransitionSpec = MotionSchemeKeyTokens.FastSpatial.value<Float>()
393     val labelProgress =
394         transition.animateFloat(label = "LabelProgress", transitionSpec = { labelTransitionSpec }) {
395             when (it) {
396                 InputPhase.Focused -> 1f
397                 InputPhase.UnfocusedEmpty -> if (showExpandedLabel) 0f else 1f
398                 InputPhase.UnfocusedNotEmpty -> 1f
399             }
400         }
401 
402     val fastOpacityTransitionSpec = MotionSchemeKeyTokens.FastEffects.value<Float>()
403     val slowOpacityTransitionSpec = MotionSchemeKeyTokens.SlowEffects.value<Float>()
404     val placeholderOpacity =
405         transition.animateFloat(
406             label = "PlaceholderOpacity",
407             transitionSpec = {
408                 if (InputPhase.Focused isTransitioningTo InputPhase.UnfocusedEmpty) {
409                     fastOpacityTransitionSpec
410                 } else if (
411                     InputPhase.UnfocusedEmpty isTransitioningTo InputPhase.Focused ||
412                         InputPhase.UnfocusedNotEmpty isTransitioningTo InputPhase.UnfocusedEmpty
413                 ) {
414                     slowOpacityTransitionSpec
415                 } else {
416                     fastOpacityTransitionSpec
417                 }
418             }
419         ) {
420             when (it) {
421                 InputPhase.Focused -> 1f
422                 InputPhase.UnfocusedEmpty -> if (showExpandedLabel) 0f else 1f
423                 InputPhase.UnfocusedNotEmpty -> 0f
424             }
425         }
426 
427     val prefixSuffixOpacity =
428         transition.animateFloat(
429             label = "PrefixSuffixOpacity",
430             transitionSpec = { fastOpacityTransitionSpec }
431         ) {
432             when (it) {
433                 InputPhase.Focused -> 1f
434                 InputPhase.UnfocusedEmpty -> if (showExpandedLabel) 0f else 1f
435                 InputPhase.UnfocusedNotEmpty -> 1f
436             }
437         }
438 
439     val colorTransitionSpec = MotionSchemeKeyTokens.FastEffects.value<Color>()
440     val labelTextStyleColor =
441         transition.animateColor(
442             transitionSpec = { colorTransitionSpec },
443             label = "LabelTextStyleColor"
444         ) {
445             when (it) {
446                 InputPhase.Focused -> focusedLabelTextStyleColor
447                 else -> unfocusedLabelTextStyleColor
448             }
449         }
450 
451     @Suppress("UnusedTransitionTargetStateParameter")
452     val labelContentColor =
453         transition.animateColor(
454             transitionSpec = { colorTransitionSpec },
455             label = "LabelContentColor",
456             targetValueByState = { labelColor }
457         )
458 
459     content(
460         labelProgress,
461         labelTextStyleColor,
462         labelContentColor,
463         placeholderOpacity,
464         prefixSuffixOpacity,
465     )
466 }
467 
468 @Composable
animateBorderStrokeAsStatenull469 internal fun animateBorderStrokeAsState(
470     enabled: Boolean,
471     isError: Boolean,
472     focused: Boolean,
473     colors: TextFieldColors,
474     focusedBorderThickness: Dp,
475     unfocusedBorderThickness: Dp
476 ): State<BorderStroke> {
477     // TODO Load the motionScheme tokens from the component tokens file
478     val targetColor = colors.indicatorColor(enabled, isError, focused)
479     val colorAnimationSpec = MotionSchemeKeyTokens.FastEffects.value<Color>()
480     val indicatorColor =
481         if (enabled) {
482             animateColorAsState(targetColor, colorAnimationSpec)
483         } else {
484             rememberUpdatedState(targetColor)
485         }
486 
487     val thicknessAnimationSpec = MotionSchemeKeyTokens.FastSpatial.value<Dp>()
488     val thickness =
489         if (enabled) {
490             val targetThickness = if (focused) focusedBorderThickness else unfocusedBorderThickness
491             animateDpAsState(targetThickness, thicknessAnimationSpec)
492         } else {
493             rememberUpdatedState(unfocusedBorderThickness)
494         }
495 
496     return rememberUpdatedState(BorderStroke(thickness.value, indicatorColor.value))
497 }
498 
499 /** An internal state used to animate a label and an indicator. */
500 private enum class InputPhase {
501     // Text field is focused
502     Focused,
503 
504     // Text field is not focused and input text is empty
505     UnfocusedEmpty,
506 
507     // Text field is not focused but input text is not empty
508     UnfocusedNotEmpty
509 }
510 
511 internal const val TextFieldId = "TextField"
512 internal const val PlaceholderId = "Hint"
513 internal const val LabelId = "Label"
514 internal const val LeadingId = "Leading"
515 internal const val TrailingId = "Trailing"
516 internal const val PrefixId = "Prefix"
517 internal const val SuffixId = "Suffix"
518 internal const val SupportingId = "Supporting"
519 internal const val ContainerId = "Container"
520 
521 // Icons are 24dp but padded to LocalMinimumInteractiveComponentSize (48dp by default), so we need
522 // to account for this visual discrepancy when applying user padding.
523 @Composable
textFieldHorizontalIconPaddingnull524 internal fun textFieldHorizontalIconPadding(): Dp {
525     val interactiveSizeOrNaN = LocalMinimumInteractiveComponentSize.current
526     val interactiveSize = if (interactiveSizeOrNaN.isUnspecified) 0.dp else interactiveSizeOrNaN
527     return ((interactiveSize - SmallIconButtonTokens.IconSize) / 2).coerceAtLeast(0.dp)
528 }
529 
530 @Composable
minimizedLabelHalfHeightnull531 internal fun minimizedLabelHalfHeight(): Dp {
532     val compositionLocalValue = MaterialTheme.typography.bodySmall.lineHeight
533     val fallbackValue = TypeScaleTokens.BodySmallLineHeight
534     val value = if (compositionLocalValue.isSp) compositionLocalValue else fallbackValue
535     return with(LocalDensity.current) { value.toDp() / 2 }
536 }
537 
538 internal val TextFieldPadding = 16.dp
539 internal val AboveLabelHorizontalPadding = 4.dp
540 internal val AboveLabelBottomPadding = 4.dp
541 internal val SupportingTopPadding = 4.dp
542 internal val PrefixSuffixTextPadding = 2.dp
543 internal val MinTextLineHeight = 24.dp
544 internal val MinFocusedLabelLineHeight = 16.dp
545 internal val MinSupportingTextLineHeight = 16.dp
546