1 /*
<lambda>null2  * Copyright 2020 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.material
18 
19 import androidx.compose.foundation.ScrollState
20 import androidx.compose.foundation.interaction.Interaction
21 import androidx.compose.foundation.interaction.MutableInteractionSource
22 import androidx.compose.foundation.layout.Box
23 import androidx.compose.foundation.layout.PaddingValues
24 import androidx.compose.foundation.layout.calculateEndPadding
25 import androidx.compose.foundation.layout.calculateStartPadding
26 import androidx.compose.foundation.layout.defaultMinSize
27 import androidx.compose.foundation.layout.padding
28 import androidx.compose.foundation.rememberScrollState
29 import androidx.compose.foundation.text.BasicTextField
30 import androidx.compose.foundation.text.KeyboardActions
31 import androidx.compose.foundation.text.KeyboardOptions
32 import androidx.compose.foundation.text.input.InputTransformation
33 import androidx.compose.foundation.text.input.KeyboardActionHandler
34 import androidx.compose.foundation.text.input.OutputTransformation
35 import androidx.compose.foundation.text.input.TextFieldBuffer
36 import androidx.compose.foundation.text.input.TextFieldLineLimits
37 import androidx.compose.foundation.text.input.TextFieldLineLimits.MultiLine
38 import androidx.compose.foundation.text.input.TextFieldLineLimits.SingleLine
39 import androidx.compose.foundation.text.input.TextFieldState
40 import androidx.compose.material.internal.subtractConstraintSafely
41 import androidx.compose.runtime.Composable
42 import androidx.compose.runtime.remember
43 import androidx.compose.ui.Alignment
44 import androidx.compose.ui.Modifier
45 import androidx.compose.ui.draw.drawWithContent
46 import androidx.compose.ui.geometry.Size
47 import androidx.compose.ui.graphics.ClipOp
48 import androidx.compose.ui.graphics.Shape
49 import androidx.compose.ui.graphics.SolidColor
50 import androidx.compose.ui.graphics.drawscope.clipRect
51 import androidx.compose.ui.graphics.takeOrElse
52 import androidx.compose.ui.layout.IntrinsicMeasurable
53 import androidx.compose.ui.layout.IntrinsicMeasureScope
54 import androidx.compose.ui.layout.Layout
55 import androidx.compose.ui.layout.Measurable
56 import androidx.compose.ui.layout.MeasurePolicy
57 import androidx.compose.ui.layout.MeasureResult
58 import androidx.compose.ui.layout.MeasureScope
59 import androidx.compose.ui.layout.Placeable
60 import androidx.compose.ui.layout.layoutId
61 import androidx.compose.ui.platform.LocalDensity
62 import androidx.compose.ui.platform.LocalLayoutDirection
63 import androidx.compose.ui.semantics.semantics
64 import androidx.compose.ui.text.TextStyle
65 import androidx.compose.ui.text.input.ImeAction
66 import androidx.compose.ui.text.input.KeyboardType
67 import androidx.compose.ui.text.input.TextFieldValue
68 import androidx.compose.ui.text.input.VisualTransformation
69 import androidx.compose.ui.unit.Constraints
70 import androidx.compose.ui.unit.IntOffset
71 import androidx.compose.ui.unit.LayoutDirection
72 import androidx.compose.ui.unit.coerceAtLeast
73 import androidx.compose.ui.unit.constrainHeight
74 import androidx.compose.ui.unit.constrainWidth
75 import androidx.compose.ui.unit.dp
76 import androidx.compose.ui.unit.offset
77 import androidx.compose.ui.unit.sp
78 import androidx.compose.ui.util.fastFirst
79 import androidx.compose.ui.util.fastFirstOrNull
80 import androidx.compose.ui.util.lerp
81 import kotlin.math.max
82 import kotlin.math.roundToInt
83 
84 /**
85  * [Material Design outlined text
86  * field](https://m2.material.io/components/text-fields#outlined-text-field).
87  *
88  * Outlined text fields have less visual emphasis than filled text fields. When they appear in
89  * places like forms, where many text fields are placed together, their reduced emphasis helps
90  * simplify the layout.
91  *
92  * ![Outlined text field
93  * image](https://developer.android.com/images/reference/androidx/compose/material/outlined-text-field.png)
94  *
95  * If you are looking for a filled version, see [TextField]. For a text field specifically designed
96  * for passwords or other secure content, see [OutlinedSecureTextField].
97  *
98  * This overload of [OutlinedTextField] uses [TextFieldState] to keep track of its text content and
99  * position of the cursor or selection.
100  *
101  * See example usage:
102  *
103  * @sample androidx.compose.material.samples.SimpleOutlinedTextFieldSample
104  * @sample androidx.compose.material.samples.OutlinedTextFieldWithInitialValueAndSelection
105  * @param state [TextFieldState] object that holds the internal editing state of this text field.
106  * @param modifier a [Modifier] for this text field
107  * @param enabled controls the enabled state of the [OutlinedTextField]. When `false`, the text
108  *   field will be neither editable nor focusable, the input of the text field will not be
109  *   selectable, visually text field will appear in the disabled UI state
110  * @param readOnly controls the editable state of the [OutlinedTextField]. When `true`, the text
111  *   field can not be modified, however, a user can focus it and copy text from it. Read-only text
112  *   fields are usually used to display pre-filled forms that user can not edit
113  * @param textStyle the style to be applied to the input text. The default [textStyle] uses the
114  *   [LocalTextStyle] defined by the theme
115  * @param label the optional label to be displayed inside the text field container. The default text
116  *   style for internal [Text] is [Typography.caption] when the text field is in focus and
117  *   [Typography.subtitle1] when the text field is not in focus
118  * @param placeholder the optional placeholder to be displayed when the text field is in focus and
119  *   the input text is empty. The default text style for internal [Text] is [Typography.subtitle1]
120  * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field
121  *   container
122  * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
123  *   container
124  * @param isError indicates if the text field's current value is in error. If set to true, the
125  *   label, bottom indicator and trailing icon by default will be displayed in error color
126  * @param inputTransformation Optional [InputTransformation] that will be used to transform changes
127  *   to the [TextFieldState] made by the user. The transformation will be applied to changes made by
128  *   hardware and software keyboard events, pasting or dropping text, accessibility services, and
129  *   tests. The transformation will _not_ be applied when changing the [state] programmatically, or
130  *   when the transformation is changed. If the transformation is changed on an existing text field,
131  *   it will be applied to the next user edit. the transformation will not immediately affect the
132  *   current [state].
133  * @param outputTransformation An [OutputTransformation] that transforms how the contents of the
134  *   text field are presented.
135  * @param keyboardOptions software keyboard options that contains configuration such as
136  *   [KeyboardType] and [ImeAction]
137  * @param onKeyboardAction Called when the user presses the action button in the input method editor
138  *   (IME), or by pressing the enter key on a hardware keyboard. By default this parameter is null,
139  *   and would execute the default behavior for a received IME Action e.g., [ImeAction.Done] would
140  *   close the keyboard, [ImeAction.Next] would switch the focus to the next focusable item on the
141  *   screen.
142  * @param lineLimits Whether the text field should be [SingleLine], scroll horizontally, and ignore
143  *   newlines; or [MultiLine] and grow and scroll vertically. If [SingleLine] is passed, all newline
144  *   characters ('\n') within the text will be replaced with regular whitespace (' '), ensuring that
145  *   the contents of the text field are presented in a single line.
146  * @param scrollState Scroll state that manages either horizontal or vertical scroll of the text
147  *   field. If [lineLimits] is [SingleLine], this text field is treated as single line with
148  *   horizontal scroll behavior. In other cases the text field becomes vertically scrollable.
149  * @param shape the shape of the text field's border
150  * @param colors [TextFieldColors] that will be used to resolve color of the text and content
151  *   (including label, placeholder, leading and trailing icons, border) for this text field in
152  *   different states. See [TextFieldDefaults.outlinedTextFieldColors]
153  * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
154  *   emitting [Interaction]s for this text field. You can use this to change the text field's
155  *   appearance or preview the text field in different states. Note that if `null` is provided,
156  *   interactions will still happen internally.
157  */
158 @Composable
159 fun OutlinedTextField(
160     state: TextFieldState,
161     modifier: Modifier = Modifier,
162     enabled: Boolean = true,
163     readOnly: Boolean = false,
164     textStyle: TextStyle = LocalTextStyle.current,
165     label: @Composable (() -> Unit)? = null,
166     placeholder: @Composable (() -> Unit)? = null,
167     leadingIcon: @Composable (() -> Unit)? = null,
168     trailingIcon: @Composable (() -> Unit)? = null,
169     isError: Boolean = false,
170     inputTransformation: InputTransformation? = null,
171     outputTransformation: OutputTransformation? = null,
172     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
173     onKeyboardAction: KeyboardActionHandler? = null,
174     lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default,
175     scrollState: ScrollState = rememberScrollState(),
176     shape: Shape = TextFieldDefaults.OutlinedTextFieldShape,
177     colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors(),
178     interactionSource: MutableInteractionSource? = null,
179 ) {
180     @Suppress("NAME_SHADOWING")
181     val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
182     // If color is not provided via the text style, use content color as a default
183     val textColor = textStyle.color.takeOrElse { colors.textColor(enabled).value }
184     val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
185 
186     val density = LocalDensity.current
187 
188     BasicTextField(
189         state = state,
190         modifier =
191             modifier
192                 .then(
193                     if (label != null) {
194                         Modifier
195                             // Merge semantics at the beginning of the modifier chain to ensure
196                             // padding is considered part of the text field.
197                             .semantics(mergeDescendants = true) {}
198                             .padding(top = with(density) { OutlinedTextFieldTopPadding.toDp() })
199                     } else {
200                         Modifier
201                     }
202                 )
203                 .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
204                 .defaultMinSize(
205                     minWidth = TextFieldDefaults.MinWidth,
206                     minHeight = TextFieldDefaults.MinHeight
207                 ),
208         enabled = enabled,
209         readOnly = readOnly,
210         textStyle = mergedTextStyle,
211         cursorBrush = SolidColor(colors.cursorColor(isError).value),
212         inputTransformation = inputTransformation,
213         outputTransformation = outputTransformation,
214         keyboardOptions = keyboardOptions,
215         onKeyboardAction = onKeyboardAction,
216         interactionSource = interactionSource,
217         scrollState = scrollState,
218         lineLimits = lineLimits,
219         decorator = { innerTextField ->
220             val textPostTransformation =
221                 if (outputTransformation == null) {
222                     state.text.toString()
223                 } else {
224                     // TODO: use constructor to create TextFieldBuffer from TextFieldState when
225                     // available
226                     lateinit var buffer: TextFieldBuffer
227                     state.edit { buffer = this }
228                     // after edit completes, mutations on buffer are ineffective
229                     with(outputTransformation) { buffer.transformOutput() }
230                     buffer.asCharSequence().toString()
231                 }
232 
233             TextFieldDefaults.OutlinedTextFieldDecorationBox(
234                 value = textPostTransformation,
235                 visualTransformation = VisualTransformation.None,
236                 innerTextField = innerTextField,
237                 placeholder = placeholder,
238                 label = label,
239                 leadingIcon = leadingIcon,
240                 trailingIcon = trailingIcon,
241                 singleLine = lineLimits == SingleLine,
242                 enabled = enabled,
243                 isError = isError,
244                 interactionSource = interactionSource,
245                 shape = shape,
246                 colors = colors,
247                 border = {
248                     TextFieldDefaults.BorderBox(enabled, isError, interactionSource, colors, shape)
249                 }
250             )
251         }
252     )
253 }
254 
255 /**
256  * [Material Design outlined text
257  * field](https://m2.material.io/components/text-fields#outlined-text-field).
258  *
259  * Outlined text fields have less visual emphasis than filled text fields. When they appear in
260  * places like forms, where many text fields are placed together, their reduced emphasis helps
261  * simplify the layout.
262  *
263  * ![Outlined text field
264  * image](https://developer.android.com/images/reference/androidx/compose/material/outlined-text-field.png)
265  *
266  * If apart from input text change you also want to observe the cursor location, selection range, or
267  * IME composition use the OutlinedTextField overload with the [TextFieldValue] parameter instead.
268  *
269  * @param value the input text to be shown in the text field
270  * @param onValueChange the callback that is triggered when the input service updates the text. An
271  *   updated text comes as a parameter of the callback
272  * @param modifier a [Modifier] for this text field
273  * @param enabled controls the enabled state of the [OutlinedTextField]. When `false`, the text
274  *   field will be neither editable nor focusable, the input of the text field will not be
275  *   selectable, visually text field will appear in the disabled UI state
276  * @param readOnly controls the editable state of the [OutlinedTextField]. When `true`, the text
277  *   field can not be modified, however, a user can focus it and copy text from it. Read-only text
278  *   fields are usually used to display pre-filled forms that user can not edit
279  * @param textStyle the style to be applied to the input text. The default [textStyle] uses the
280  *   [LocalTextStyle] defined by the theme
281  * @param label the optional label to be displayed inside the text field container. The default text
282  *   style for internal [Text] is [Typography.caption] when the text field is in focus and
283  *   [Typography.subtitle1] when the text field is not in focus
284  * @param placeholder the optional placeholder to be displayed when the text field is in focus and
285  *   the input text is empty. The default text style for internal [Text] is [Typography.subtitle1]
286  * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field
287  *   container
288  * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
289  *   container
290  * @param isError indicates if the text field's current value is in error. If set to true, the
291  *   label, bottom indicator and trailing icon by default will be displayed in error color
292  * @param visualTransformation transforms the visual representation of the input [value] For
293  *   example, you can use
294  *   [PasswordVisualTransformation][androidx.compose.ui.text.input.PasswordVisualTransformation] to
295  *   create a password text field. By default no visual transformation is applied
296  * @param keyboardOptions software keyboard options that contains configuration such as
297  *   [KeyboardType] and [ImeAction]
298  * @param keyboardActions when the input service emits an IME action, the corresponding callback is
299  *   called. Note that this IME action may be different from what you specified in
300  *   [KeyboardOptions.imeAction]
301  * @param singleLine when set to true, this text field becomes a single horizontally scrolling text
302  *   field instead of wrapping onto multiple lines. The keyboard will be informed to not show the
303  *   return key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines
304  *   attribute will be automatically set to 1
305  * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
306  *   that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
307  * @param minLines the minimum height in terms of minimum number of visible lines. It is required
308  *   that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
309  * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
310  *   emitting [Interaction]s for this text field. You can use this to change the text field's
311  *   appearance or preview the text field in different states. Note that if `null` is provided,
312  *   interactions will still happen internally.
313  * @param shape the shape of the text field's border
314  * @param colors [TextFieldColors] that will be used to resolve color of the text and content
315  *   (including label, placeholder, leading and trailing icons, border) for this text field in
316  *   different states. See [TextFieldDefaults.outlinedTextFieldColors]
317  */
318 @Composable
OutlinedTextFieldnull319 fun OutlinedTextField(
320     value: String,
321     onValueChange: (String) -> Unit,
322     modifier: Modifier = Modifier,
323     enabled: Boolean = true,
324     readOnly: Boolean = false,
325     textStyle: TextStyle = LocalTextStyle.current,
326     label: @Composable (() -> Unit)? = null,
327     placeholder: @Composable (() -> Unit)? = null,
328     leadingIcon: @Composable (() -> Unit)? = null,
329     trailingIcon: @Composable (() -> Unit)? = null,
330     isError: Boolean = false,
331     visualTransformation: VisualTransformation = VisualTransformation.None,
332     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
333     keyboardActions: KeyboardActions = KeyboardActions.Default,
334     singleLine: Boolean = false,
335     maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
336     minLines: Int = 1,
337     interactionSource: MutableInteractionSource? = null,
338     shape: Shape = TextFieldDefaults.OutlinedTextFieldShape,
339     colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
340 ) {
341     @Suppress("NAME_SHADOWING")
342     val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
343     // If color is not provided via the text style, use content color as a default
344     val textColor = textStyle.color.takeOrElse { colors.textColor(enabled).value }
345     val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
346 
347     val density = LocalDensity.current
348 
349     BasicTextField(
350         value = value,
351         modifier =
352             modifier
353                 .then(
354                     if (label != null) {
355                         Modifier
356                             // Merge semantics at the beginning of the modifier chain to ensure
357                             // padding is considered part of the text field.
358                             .semantics(mergeDescendants = true) {}
359                             .padding(top = with(density) { OutlinedTextFieldTopPadding.toDp() })
360                     } else {
361                         Modifier
362                     }
363                 )
364                 .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
365                 .defaultMinSize(
366                     minWidth = TextFieldDefaults.MinWidth,
367                     minHeight = TextFieldDefaults.MinHeight
368                 ),
369         onValueChange = onValueChange,
370         enabled = enabled,
371         readOnly = readOnly,
372         textStyle = mergedTextStyle,
373         cursorBrush = SolidColor(colors.cursorColor(isError).value),
374         visualTransformation = visualTransformation,
375         keyboardOptions = keyboardOptions,
376         keyboardActions = keyboardActions,
377         interactionSource = interactionSource,
378         singleLine = singleLine,
379         maxLines = maxLines,
380         minLines = minLines,
381         decorationBox =
382             @Composable { innerTextField ->
383                 TextFieldDefaults.OutlinedTextFieldDecorationBox(
384                     value = value,
385                     visualTransformation = visualTransformation,
386                     innerTextField = innerTextField,
387                     placeholder = placeholder,
388                     label = label,
389                     leadingIcon = leadingIcon,
390                     trailingIcon = trailingIcon,
391                     singleLine = singleLine,
392                     enabled = enabled,
393                     isError = isError,
394                     interactionSource = interactionSource,
395                     shape = shape,
396                     colors = colors,
397                     border = {
398                         TextFieldDefaults.BorderBox(
399                             enabled,
400                             isError,
401                             interactionSource,
402                             colors,
403                             shape
404                         )
405                     }
406                 )
407             }
408     )
409 }
410 
411 @Deprecated(
412     "Maintained for binary compatibility. Use version with minLines instead",
413     level = DeprecationLevel.HIDDEN
414 )
415 @Composable
OutlinedTextFieldnull416 fun OutlinedTextField(
417     value: String,
418     onValueChange: (String) -> Unit,
419     modifier: Modifier = Modifier,
420     enabled: Boolean = true,
421     readOnly: Boolean = false,
422     textStyle: TextStyle = LocalTextStyle.current,
423     label: @Composable (() -> Unit)? = null,
424     placeholder: @Composable (() -> Unit)? = null,
425     leadingIcon: @Composable (() -> Unit)? = null,
426     trailingIcon: @Composable (() -> Unit)? = null,
427     isError: Boolean = false,
428     visualTransformation: VisualTransformation = VisualTransformation.None,
429     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
430     keyboardActions: KeyboardActions = KeyboardActions.Default,
431     singleLine: Boolean = false,
432     maxLines: Int = Int.MAX_VALUE,
433     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
434     shape: Shape = TextFieldDefaults.OutlinedTextFieldShape,
435     colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
436 ) {
437     OutlinedTextField(
438         value,
439         onValueChange,
440         modifier,
441         enabled,
442         readOnly,
443         textStyle,
444         label,
445         placeholder,
446         leadingIcon,
447         trailingIcon,
448         isError,
449         visualTransformation,
450         keyboardOptions,
451         keyboardActions,
452         singleLine,
453         maxLines,
454         1,
455         interactionSource,
456         shape,
457         colors
458     )
459 }
460 
461 /**
462  * [Material Design outlined text
463  * field](https://m2.material.io/components/text-fields#outlined-text-field).
464  *
465  * Outlined text fields have less visual emphasis than filled text fields. When they appear in
466  * places like forms, where many text fields are placed together, their reduced emphasis helps
467  * simplify the layout.
468  *
469  * ![Outlined text field
470  * image](https://developer.android.com/images/reference/androidx/compose/material/outlined-text-field.png)
471  *
472  * This overload provides access to the input text, cursor position and selection range and IME
473  * composition. If you only want to observe an input text change, use the OutlinedTextField overload
474  * with the [String] parameter instead.
475  *
476  * @param value the input [TextFieldValue] to be shown in the text field
477  * @param onValueChange the callback that is triggered when the input service updates values in
478  *   [TextFieldValue]. An updated [TextFieldValue] comes as a parameter of the callback
479  * @param modifier a [Modifier] for this text field
480  * @param enabled controls the enabled state of the [OutlinedTextField]. When `false`, the text
481  *   field will be neither editable nor focusable, the input of the text field will not be
482  *   selectable, visually text field will appear in the disabled UI state
483  * @param readOnly controls the editable state of the [OutlinedTextField]. When `true`, the text
484  *   field can not be modified, however, a user can focus it and copy text from it. Read-only text
485  *   fields are usually used to display pre-filled forms that user can not edit
486  * @param textStyle the style to be applied to the input text. The default [textStyle] uses the
487  *   [LocalTextStyle] defined by the theme
488  * @param label the optional label to be displayed inside the text field container. The default text
489  *   style for internal [Text] is [Typography.caption] when the text field is in focus and
490  *   [Typography.subtitle1] when the text field is not in focus
491  * @param placeholder the optional placeholder to be displayed when the text field is in focus and
492  *   the input text is empty. The default text style for internal [Text] is [Typography.subtitle1]
493  * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field
494  *   container
495  * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
496  *   container
497  * @param isError indicates if the text field's current value is in error state. If set to true, the
498  *   label, bottom indicator and trailing icon by default will be displayed in error color
499  * @param visualTransformation transforms the visual representation of the input [value] For
500  *   example, you can use
501  *   [PasswordVisualTransformation][androidx.compose.ui.text.input.PasswordVisualTransformation] to
502  *   create a password text field. By default no visual transformation is applied
503  * @param keyboardOptions software keyboard options that contains configuration such as
504  *   [KeyboardType] and [ImeAction]
505  * @param keyboardActions when the input service emits an IME action, the corresponding callback is
506  *   called. Note that this IME action may be different from what you specified in
507  *   [KeyboardOptions.imeAction]
508  * @param singleLine when set to true, this text field becomes a single horizontally scrolling text
509  *   field instead of wrapping onto multiple lines. The keyboard will be informed to not show the
510  *   return key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines
511  *   attribute will be automatically set to 1
512  * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
513  *   that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
514  * @param minLines the minimum height in terms of minimum number of visible lines. It is required
515  *   that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
516  * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
517  *   emitting [Interaction]s for this text field. You can use this to change the text field's
518  *   appearance or preview the text field in different states. Note that if `null` is provided,
519  *   interactions will still happen internally.
520  * @param shape the shape of the text field's border
521  * @param colors [TextFieldColors] that will be used to resolve color of the text and content
522  *   (including label, placeholder, leading and trailing icons, border) for this text field in
523  *   different states. See [TextFieldDefaults.outlinedTextFieldColors]
524  */
525 @Composable
OutlinedTextFieldnull526 fun OutlinedTextField(
527     value: TextFieldValue,
528     onValueChange: (TextFieldValue) -> Unit,
529     modifier: Modifier = Modifier,
530     enabled: Boolean = true,
531     readOnly: Boolean = false,
532     textStyle: TextStyle = LocalTextStyle.current,
533     label: @Composable (() -> Unit)? = null,
534     placeholder: @Composable (() -> Unit)? = null,
535     leadingIcon: @Composable (() -> Unit)? = null,
536     trailingIcon: @Composable (() -> Unit)? = null,
537     isError: Boolean = false,
538     visualTransformation: VisualTransformation = VisualTransformation.None,
539     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
540     keyboardActions: KeyboardActions = KeyboardActions(),
541     singleLine: Boolean = false,
542     maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
543     minLines: Int = 1,
544     interactionSource: MutableInteractionSource? = null,
545     shape: Shape = TextFieldDefaults.OutlinedTextFieldShape,
546     colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
547 ) {
548     @Suppress("NAME_SHADOWING")
549     val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
550     // If color is not provided via the text style, use content color as a default
551     val textColor = textStyle.color.takeOrElse { colors.textColor(enabled).value }
552     val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
553 
554     val density = LocalDensity.current
555 
556     BasicTextField(
557         value = value,
558         modifier =
559             modifier
560                 .then(
561                     if (label != null) {
562                         Modifier
563                             // Merge semantics at the beginning of the modifier chain to ensure
564                             // padding is considered part of the text field.
565                             .semantics(mergeDescendants = true) {}
566                             .padding(top = with(density) { OutlinedTextFieldTopPadding.toDp() })
567                     } else {
568                         Modifier
569                     }
570                 )
571                 .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
572                 .defaultMinSize(
573                     minWidth = TextFieldDefaults.MinWidth,
574                     minHeight = TextFieldDefaults.MinHeight
575                 ),
576         onValueChange = onValueChange,
577         enabled = enabled,
578         readOnly = readOnly,
579         textStyle = mergedTextStyle,
580         cursorBrush = SolidColor(colors.cursorColor(isError).value),
581         visualTransformation = visualTransformation,
582         keyboardOptions = keyboardOptions,
583         keyboardActions = keyboardActions,
584         interactionSource = interactionSource,
585         singleLine = singleLine,
586         maxLines = maxLines,
587         minLines = minLines,
588         decorationBox =
589             @Composable { innerTextField ->
590                 TextFieldDefaults.OutlinedTextFieldDecorationBox(
591                     value = value.text,
592                     visualTransformation = visualTransformation,
593                     innerTextField = innerTextField,
594                     placeholder = placeholder,
595                     label = label,
596                     leadingIcon = leadingIcon,
597                     trailingIcon = trailingIcon,
598                     singleLine = singleLine,
599                     enabled = enabled,
600                     isError = isError,
601                     interactionSource = interactionSource,
602                     shape = shape,
603                     colors = colors,
604                     border = {
605                         TextFieldDefaults.BorderBox(
606                             enabled,
607                             isError,
608                             interactionSource,
609                             colors,
610                             shape
611                         )
612                     }
613                 )
614             }
615     )
616 }
617 
618 @Deprecated(
619     "Maintained for binary compatibility. Use version with minLines instead",
620     level = DeprecationLevel.HIDDEN
621 )
622 @Composable
OutlinedTextFieldnull623 fun OutlinedTextField(
624     value: TextFieldValue,
625     onValueChange: (TextFieldValue) -> Unit,
626     modifier: Modifier = Modifier,
627     enabled: Boolean = true,
628     readOnly: Boolean = false,
629     textStyle: TextStyle = LocalTextStyle.current,
630     label: @Composable (() -> Unit)? = null,
631     placeholder: @Composable (() -> Unit)? = null,
632     leadingIcon: @Composable (() -> Unit)? = null,
633     trailingIcon: @Composable (() -> Unit)? = null,
634     isError: Boolean = false,
635     visualTransformation: VisualTransformation = VisualTransformation.None,
636     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
637     keyboardActions: KeyboardActions = KeyboardActions(),
638     singleLine: Boolean = false,
639     maxLines: Int = Int.MAX_VALUE,
640     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
641     shape: Shape = TextFieldDefaults.OutlinedTextFieldShape,
642     colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
643 ) {
644     OutlinedTextField(
645         value,
646         onValueChange,
647         modifier,
648         enabled,
649         readOnly,
650         textStyle,
651         label,
652         placeholder,
653         leadingIcon,
654         trailingIcon,
655         isError,
656         visualTransformation,
657         keyboardOptions,
658         keyboardActions,
659         singleLine,
660         maxLines,
661         1,
662         interactionSource,
663         shape,
664         colors
665     )
666 }
667 
668 /**
669  * Layout of the leading and trailing icons and the text field, label and placeholder in
670  * [OutlinedTextField]. It doesn't use Row to position the icons and middle part because label
671  * should not be positioned in the middle part. \
672  */
673 @Composable
674 internal fun OutlinedTextFieldLayout(
675     modifier: Modifier,
676     textField: @Composable () -> Unit,
677     placeholder: @Composable ((Modifier) -> Unit)?,
678     label: @Composable (() -> Unit)?,
679     leading: @Composable (() -> Unit)?,
680     trailing: @Composable (() -> Unit)?,
681     singleLine: Boolean,
682     animationProgress: Float,
683     onLabelMeasured: (Size) -> Unit,
684     border: @Composable () -> Unit,
685     paddingValues: PaddingValues
686 ) {
687     val measurePolicy =
<lambda>null688         remember(onLabelMeasured, singleLine, animationProgress, paddingValues) {
689             OutlinedTextFieldMeasurePolicy(
690                 onLabelMeasured,
691                 singleLine,
692                 animationProgress,
693                 paddingValues
694             )
695         }
696     val layoutDirection = LocalLayoutDirection.current
697     Layout(
698         modifier = modifier,
<lambda>null699         content = {
700             // We use additional box here to place an outlined cutout border as a sibling after the
701             // rest of UI. This allows us to use Modifier.border to draw an outline on top of the
702             // text field. We can't use the border modifier directly on the IconsWithTextFieldLayout
703             // as we also need to do the clipping (to form the cutout) which should not affect
704             // the rest of text field UI
705             border()
706 
707             if (leading != null) {
708                 Box(
709                     modifier = Modifier.layoutId(LeadingId).minimumInteractiveComponentSize(),
710                     contentAlignment = Alignment.Center
711                 ) {
712                     leading()
713                 }
714             }
715             if (trailing != null) {
716                 Box(
717                     modifier = Modifier.layoutId(TrailingId).minimumInteractiveComponentSize(),
718                     contentAlignment = Alignment.Center
719                 ) {
720                     trailing()
721                 }
722             }
723 
724             val startTextFieldPadding = paddingValues.calculateStartPadding(layoutDirection)
725             val endTextFieldPadding = paddingValues.calculateEndPadding(layoutDirection)
726             val padding =
727                 Modifier.padding(
728                     start =
729                         if (leading != null) {
730                             (startTextFieldPadding - HorizontalIconPadding).coerceAtLeast(0.dp)
731                         } else {
732                             startTextFieldPadding
733                         },
734                     end =
735                         if (trailing != null) {
736                             (endTextFieldPadding - HorizontalIconPadding).coerceAtLeast(0.dp)
737                         } else {
738                             endTextFieldPadding
739                         }
740                 )
741             if (placeholder != null) {
742                 placeholder(Modifier.layoutId(PlaceholderId).then(padding))
743             }
744 
745             Box(
746                 modifier = Modifier.layoutId(TextFieldId).then(padding),
747                 propagateMinConstraints = true
748             ) {
749                 textField()
750             }
751 
752             if (label != null) {
753                 Box(modifier = Modifier.layoutId(LabelId)) { label() }
754             }
755         },
756         measurePolicy = measurePolicy
757     )
758 }
759 
760 private class OutlinedTextFieldMeasurePolicy(
761     private val onLabelMeasured: (Size) -> Unit,
762     private val singleLine: Boolean,
763     private val animationProgress: Float,
764     private val paddingValues: PaddingValues
765 ) : MeasurePolicy {
measurenull766     override fun MeasureScope.measure(
767         measurables: List<Measurable>,
768         constraints: Constraints
769     ): MeasureResult {
770         // used to calculate the constraints for measuring elements that will be placed in a row
771         var occupiedSpaceHorizontally = 0
772         val bottomPadding = paddingValues.calculateBottomPadding().roundToPx()
773 
774         // measure leading icon
775         val relaxedConstraints = constraints.copy(minWidth = 0, minHeight = 0)
776         val leadingPlaceable =
777             measurables.fastFirstOrNull { it.layoutId == LeadingId }?.measure(relaxedConstraints)
778         occupiedSpaceHorizontally += widthOrZero(leadingPlaceable)
779 
780         // measure trailing icon
781         val trailingPlaceable =
782             measurables
783                 .fastFirstOrNull { it.layoutId == TrailingId }
784                 ?.measure(relaxedConstraints.offset(horizontal = -occupiedSpaceHorizontally))
785         occupiedSpaceHorizontally += widthOrZero(trailingPlaceable)
786 
787         // measure label
788         val labelHorizontalPaddingOffset =
789             paddingValues.calculateLeftPadding(layoutDirection).roundToPx() +
790                 paddingValues.calculateRightPadding(layoutDirection).roundToPx()
791         val labelConstraints =
792             relaxedConstraints.offset(
793                 horizontal =
794                     lerp(
795                         -occupiedSpaceHorizontally - labelHorizontalPaddingOffset,
796                         -labelHorizontalPaddingOffset,
797                         animationProgress,
798                     ),
799                 vertical = -bottomPadding
800             )
801         val labelPlaceable =
802             measurables.fastFirstOrNull { it.layoutId == LabelId }?.measure(labelConstraints)
803         val labelSize =
804             labelPlaceable?.let { Size(it.width.toFloat(), it.height.toFloat()) } ?: Size.Zero
805         onLabelMeasured(labelSize)
806 
807         // measure text field
808         // on top we offset either by default padding or by label's half height if its too big
809         // minHeight must not be set to 0 due to how foundation TextField treats zero minHeight
810         val topPadding =
811             max(heightOrZero(labelPlaceable) / 2, paddingValues.calculateTopPadding().roundToPx())
812         val textConstraints =
813             constraints
814                 .offset(
815                     horizontal = -occupiedSpaceHorizontally,
816                     vertical = -bottomPadding - topPadding
817                 )
818                 .copy(minHeight = 0)
819         val textFieldPlaceable =
820             measurables.fastFirst { it.layoutId == TextFieldId }.measure(textConstraints)
821 
822         // measure placeholder
823         val placeholderConstraints = textConstraints.copy(minWidth = 0)
824         val placeholderPlaceable =
825             measurables
826                 .fastFirstOrNull { it.layoutId == PlaceholderId }
827                 ?.measure(placeholderConstraints)
828 
829         val width =
830             calculateWidth(
831                 leadingPlaceableWidth = widthOrZero(leadingPlaceable),
832                 trailingPlaceableWidth = widthOrZero(trailingPlaceable),
833                 textFieldPlaceableWidth = textFieldPlaceable.width,
834                 labelPlaceableWidth = widthOrZero(labelPlaceable),
835                 placeholderPlaceableWidth = widthOrZero(placeholderPlaceable),
836                 animationProgress = animationProgress,
837                 constraints = constraints,
838                 density = density,
839                 paddingValues = paddingValues,
840             )
841         val height =
842             calculateHeight(
843                 leadingPlaceableHeight = heightOrZero(leadingPlaceable),
844                 trailingPlaceableHeight = heightOrZero(trailingPlaceable),
845                 textFieldPlaceableHeight = textFieldPlaceable.height,
846                 labelPlaceableHeight = heightOrZero(labelPlaceable),
847                 placeholderPlaceableHeight = heightOrZero(placeholderPlaceable),
848                 animationProgress = animationProgress,
849                 constraints = constraints,
850                 density = density,
851                 paddingValues = paddingValues,
852             )
853 
854         val borderPlaceable =
855             measurables
856                 .fastFirst { it.layoutId == BorderId }
857                 .measure(
858                     Constraints(
859                         minWidth = if (width != Constraints.Infinity) width else 0,
860                         maxWidth = width,
861                         minHeight = if (height != Constraints.Infinity) height else 0,
862                         maxHeight = height
863                     )
864                 )
865         return layout(width, height) {
866             place(
867                 height,
868                 width,
869                 leadingPlaceable,
870                 trailingPlaceable,
871                 textFieldPlaceable,
872                 labelPlaceable,
873                 placeholderPlaceable,
874                 borderPlaceable,
875                 animationProgress,
876                 singleLine,
877                 density,
878                 layoutDirection,
879                 paddingValues
880             )
881         }
882     }
883 
maxIntrinsicHeightnull884     override fun IntrinsicMeasureScope.maxIntrinsicHeight(
885         measurables: List<IntrinsicMeasurable>,
886         width: Int
887     ): Int {
888         return intrinsicHeight(measurables, width) { intrinsicMeasurable, w ->
889             intrinsicMeasurable.maxIntrinsicHeight(w)
890         }
891     }
892 
minIntrinsicHeightnull893     override fun IntrinsicMeasureScope.minIntrinsicHeight(
894         measurables: List<IntrinsicMeasurable>,
895         width: Int
896     ): Int {
897         return intrinsicHeight(measurables, width) { intrinsicMeasurable, w ->
898             intrinsicMeasurable.minIntrinsicHeight(w)
899         }
900     }
901 
maxIntrinsicWidthnull902     override fun IntrinsicMeasureScope.maxIntrinsicWidth(
903         measurables: List<IntrinsicMeasurable>,
904         height: Int
905     ): Int {
906         return intrinsicWidth(measurables, height) { intrinsicMeasurable, h ->
907             intrinsicMeasurable.maxIntrinsicWidth(h)
908         }
909     }
910 
minIntrinsicWidthnull911     override fun IntrinsicMeasureScope.minIntrinsicWidth(
912         measurables: List<IntrinsicMeasurable>,
913         height: Int
914     ): Int {
915         return intrinsicWidth(measurables, height) { intrinsicMeasurable, h ->
916             intrinsicMeasurable.minIntrinsicWidth(h)
917         }
918     }
919 
intrinsicWidthnull920     private fun IntrinsicMeasureScope.intrinsicWidth(
921         measurables: List<IntrinsicMeasurable>,
922         height: Int,
923         intrinsicMeasurer: (IntrinsicMeasurable, Int) -> Int
924     ): Int {
925         val textFieldWidth =
926             intrinsicMeasurer(measurables.fastFirst { it.layoutId == TextFieldId }, height)
927         val labelWidth =
928             measurables
929                 .fastFirstOrNull { it.layoutId == LabelId }
930                 ?.let { intrinsicMeasurer(it, height) } ?: 0
931         val trailingWidth =
932             measurables
933                 .fastFirstOrNull { it.layoutId == TrailingId }
934                 ?.let { intrinsicMeasurer(it, height) } ?: 0
935         val leadingWidth =
936             measurables
937                 .fastFirstOrNull { it.layoutId == LeadingId }
938                 ?.let { intrinsicMeasurer(it, height) } ?: 0
939         val placeholderWidth =
940             measurables
941                 .fastFirstOrNull { it.layoutId == PlaceholderId }
942                 ?.let { intrinsicMeasurer(it, height) } ?: 0
943         return calculateWidth(
944             leadingPlaceableWidth = leadingWidth,
945             trailingPlaceableWidth = trailingWidth,
946             textFieldPlaceableWidth = textFieldWidth,
947             labelPlaceableWidth = labelWidth,
948             placeholderPlaceableWidth = placeholderWidth,
949             animationProgress = animationProgress,
950             constraints = Constraints(),
951             density = density,
952             paddingValues = paddingValues,
953         )
954     }
955 
intrinsicHeightnull956     private fun IntrinsicMeasureScope.intrinsicHeight(
957         measurables: List<IntrinsicMeasurable>,
958         width: Int,
959         intrinsicMeasurer: (IntrinsicMeasurable, Int) -> Int
960     ): Int {
961         var remainingWidth = width
962         val leadingHeight =
963             measurables
964                 .fastFirstOrNull { it.layoutId == LeadingId }
965                 ?.let {
966                     remainingWidth =
967                         remainingWidth.subtractConstraintSafely(
968                             it.maxIntrinsicWidth(Constraints.Infinity)
969                         )
970                     intrinsicMeasurer(it, width)
971                 } ?: 0
972         val trailingHeight =
973             measurables
974                 .fastFirstOrNull { it.layoutId == TrailingId }
975                 ?.let {
976                     remainingWidth =
977                         remainingWidth.subtractConstraintSafely(
978                             it.maxIntrinsicWidth(Constraints.Infinity)
979                         )
980                     intrinsicMeasurer(it, width)
981                 } ?: 0
982 
983         val labelHeight =
984             measurables
985                 .fastFirstOrNull { it.layoutId == LabelId }
986                 ?.let { intrinsicMeasurer(it, lerp(remainingWidth, width, animationProgress)) } ?: 0
987 
988         val textFieldHeight =
989             intrinsicMeasurer(measurables.fastFirst { it.layoutId == TextFieldId }, remainingWidth)
990         val placeholderHeight =
991             measurables
992                 .fastFirstOrNull { it.layoutId == PlaceholderId }
993                 ?.let { intrinsicMeasurer(it, remainingWidth) } ?: 0
994 
995         return calculateHeight(
996             leadingPlaceableHeight = leadingHeight,
997             trailingPlaceableHeight = trailingHeight,
998             textFieldPlaceableHeight = textFieldHeight,
999             labelPlaceableHeight = labelHeight,
1000             placeholderPlaceableHeight = placeholderHeight,
1001             animationProgress = animationProgress,
1002             constraints = Constraints(),
1003             density = density,
1004             paddingValues = paddingValues
1005         )
1006     }
1007 }
1008 
1009 /**
1010  * Calculate the width of the [OutlinedTextField] given all elements that should be placed inside
1011  */
calculateWidthnull1012 private fun calculateWidth(
1013     leadingPlaceableWidth: Int,
1014     trailingPlaceableWidth: Int,
1015     textFieldPlaceableWidth: Int,
1016     labelPlaceableWidth: Int,
1017     placeholderPlaceableWidth: Int,
1018     animationProgress: Float,
1019     constraints: Constraints,
1020     density: Float,
1021     paddingValues: PaddingValues,
1022 ): Int {
1023     val middleSection =
1024         maxOf(
1025             textFieldPlaceableWidth,
1026             lerp(labelPlaceableWidth, 0, animationProgress),
1027             placeholderPlaceableWidth
1028         )
1029     val wrappedWidth = leadingPlaceableWidth + middleSection + trailingPlaceableWidth
1030 
1031     // Actual LayoutDirection doesn't matter; we only need the sum
1032     val labelHorizontalPadding =
1033         (paddingValues.calculateLeftPadding(LayoutDirection.Ltr) +
1034                 paddingValues.calculateRightPadding(LayoutDirection.Ltr))
1035             .value * density
1036     val focusedLabelWidth =
1037         ((labelPlaceableWidth + labelHorizontalPadding) * animationProgress).roundToInt()
1038 
1039     return constraints.constrainWidth(max(wrappedWidth, focusedLabelWidth))
1040 }
1041 
1042 /**
1043  * Calculate the height of the [OutlinedTextField] given all elements that should be placed inside
1044  */
calculateHeightnull1045 private fun calculateHeight(
1046     leadingPlaceableHeight: Int,
1047     trailingPlaceableHeight: Int,
1048     textFieldPlaceableHeight: Int,
1049     labelPlaceableHeight: Int,
1050     placeholderPlaceableHeight: Int,
1051     animationProgress: Float,
1052     constraints: Constraints,
1053     density: Float,
1054     paddingValues: PaddingValues
1055 ): Int {
1056     val inputFieldHeight =
1057         maxOf(
1058             textFieldPlaceableHeight,
1059             placeholderPlaceableHeight,
1060             lerp(labelPlaceableHeight, 0, animationProgress),
1061         )
1062     val topPadding = paddingValues.calculateTopPadding().value * density
1063     val actualTopPadding =
1064         lerp(
1065             topPadding,
1066             max(topPadding, labelPlaceableHeight / 2f),
1067             animationProgress,
1068         )
1069     val bottomPadding = paddingValues.calculateBottomPadding().value * density
1070     val middleSectionHeight = actualTopPadding + inputFieldHeight + bottomPadding
1071 
1072     return constraints.constrainHeight(
1073         maxOf(leadingPlaceableHeight, trailingPlaceableHeight, middleSectionHeight.roundToInt())
1074     )
1075 }
1076 
1077 /**
1078  * Places the provided text field, placeholder, label, optional leading and trailing icons inside
1079  * the [OutlinedTextField]
1080  */
Placeablenull1081 private fun Placeable.PlacementScope.place(
1082     height: Int,
1083     width: Int,
1084     leadingPlaceable: Placeable?,
1085     trailingPlaceable: Placeable?,
1086     textFieldPlaceable: Placeable,
1087     labelPlaceable: Placeable?,
1088     placeholderPlaceable: Placeable?,
1089     borderPlaceable: Placeable,
1090     animationProgress: Float,
1091     singleLine: Boolean,
1092     density: Float,
1093     layoutDirection: LayoutDirection,
1094     paddingValues: PaddingValues
1095 ) {
1096     val topPadding = (paddingValues.calculateTopPadding().value * density).roundToInt()
1097     val startPadding =
1098         (paddingValues.calculateStartPadding(layoutDirection).value * density).roundToInt()
1099 
1100     val iconPadding = HorizontalIconPadding.value * density
1101 
1102     // placed center vertically and to the start edge horizontally
1103     leadingPlaceable?.placeRelative(
1104         0,
1105         Alignment.CenterVertically.align(leadingPlaceable.height, height)
1106     )
1107 
1108     // placed center vertically and to the end edge horizontally
1109     trailingPlaceable?.placeRelative(
1110         width - trailingPlaceable.width,
1111         Alignment.CenterVertically.align(trailingPlaceable.height, height)
1112     )
1113 
1114     // label position is animated
1115     // in single line text field label is centered vertically before animation starts
1116     labelPlaceable?.let {
1117         val startPositionY =
1118             if (singleLine) {
1119                 Alignment.CenterVertically.align(it.height, height)
1120             } else {
1121                 topPadding
1122             }
1123         val positionY = lerp(startPositionY, -(it.height / 2), animationProgress)
1124         val positionX =
1125             (if (leadingPlaceable == null) {
1126                     0f
1127                 } else {
1128                     (widthOrZero(leadingPlaceable) - iconPadding) * (1 - animationProgress)
1129                 })
1130                 .roundToInt() + startPadding
1131         it.placeRelative(positionX, positionY)
1132     }
1133 
1134     // placed center vertically and after the leading icon horizontally if single line text field
1135     // placed to the top with padding for multi line text field
1136     val textVerticalPosition =
1137         max(
1138             if (singleLine) {
1139                 Alignment.CenterVertically.align(textFieldPlaceable.height, height)
1140             } else {
1141                 topPadding
1142             },
1143             heightOrZero(labelPlaceable) / 2
1144         )
1145     textFieldPlaceable.placeRelative(widthOrZero(leadingPlaceable), textVerticalPosition)
1146 
1147     // placed similar to the input text above
1148     placeholderPlaceable?.let {
1149         val placeholderVerticalPosition =
1150             max(
1151                 if (singleLine) {
1152                     Alignment.CenterVertically.align(it.height, height)
1153                 } else {
1154                     topPadding
1155                 },
1156                 heightOrZero(labelPlaceable) / 2
1157             )
1158         it.placeRelative(widthOrZero(leadingPlaceable), placeholderVerticalPosition)
1159     }
1160 
1161     // place border
1162     borderPlaceable.place(IntOffset.Zero)
1163 }
1164 
outlineCutoutnull1165 internal fun Modifier.outlineCutout(labelSize: Size, paddingValues: PaddingValues) =
1166     this.drawWithContent {
1167         val labelWidth = labelSize.width
1168         if (labelWidth > 0f) {
1169             val innerPadding = OutlinedTextFieldInnerPadding.toPx()
1170             val leftLtr = paddingValues.calculateLeftPadding(layoutDirection).toPx() - innerPadding
1171             val rightLtr = leftLtr + labelWidth + 2 * innerPadding
1172             val left =
1173                 when (layoutDirection) {
1174                     LayoutDirection.Rtl -> size.width - rightLtr
1175                     else -> leftLtr.coerceAtLeast(0f)
1176                 }
1177             val right =
1178                 when (layoutDirection) {
1179                     LayoutDirection.Rtl -> size.width - leftLtr.coerceAtLeast(0f)
1180                     else -> rightLtr
1181                 }
1182             val labelHeight = labelSize.height
1183             // using label height as a cutout area to make sure that no hairline artifacts are
1184             // left when we clip the border
1185             clipRect(left, -labelHeight / 2, right, labelHeight / 2, ClipOp.Difference) {
1186                 this@drawWithContent.drawContent()
1187             }
1188         } else {
1189             this@drawWithContent.drawContent()
1190         }
1191     }
1192 
1193 private val OutlinedTextFieldInnerPadding = 4.dp
1194 
1195 /**
1196  * In the focused state, the top half of the label sticks out above the text field. This default
1197  * padding is a best-effort approximation to keep the label from overlapping with the content above
1198  * it. It is sufficient when the label is a single line and developers do not override the label's
1199  * font size/style. Otherwise, developers will need to add additional padding themselves.
1200  */
1201 internal val OutlinedTextFieldTopPadding = 8.sp
1202 
1203 internal const val BorderId = "border"
1204