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 @file:Suppress("DEPRECATION")
18 
19 package androidx.compose.foundation.text
20 
21 import androidx.compose.foundation.ComposeFoundationFlags
22 import androidx.compose.foundation.ExperimentalFoundationApi
23 import androidx.compose.foundation.gestures.Orientation
24 import androidx.compose.foundation.gestures.detectTapGestures
25 import androidx.compose.foundation.interaction.Interaction
26 import androidx.compose.foundation.interaction.MutableInteractionSource
27 import androidx.compose.foundation.layout.Box
28 import androidx.compose.foundation.layout.heightIn
29 import androidx.compose.foundation.relocation.BringIntoViewRequester
30 import androidx.compose.foundation.relocation.bringIntoViewRequester
31 import androidx.compose.foundation.text.handwriting.stylusHandwriting
32 import androidx.compose.foundation.text.input.internal.CoreTextFieldSemanticsModifier
33 import androidx.compose.foundation.text.input.internal.createLegacyPlatformTextInputServiceAdapter
34 import androidx.compose.foundation.text.input.internal.legacyTextInputAdapter
35 import androidx.compose.foundation.text.selection.LocalTextSelectionColors
36 import androidx.compose.foundation.text.selection.OffsetProvider
37 import androidx.compose.foundation.text.selection.SelectionHandleAnchor
38 import androidx.compose.foundation.text.selection.SelectionHandleInfo
39 import androidx.compose.foundation.text.selection.SelectionHandleInfoKey
40 import androidx.compose.foundation.text.selection.SimpleLayout
41 import androidx.compose.foundation.text.selection.TextFieldSelectionHandle
42 import androidx.compose.foundation.text.selection.TextFieldSelectionManager
43 import androidx.compose.foundation.text.selection.addBasicTextFieldTextContextMenuComponents
44 import androidx.compose.foundation.text.selection.isSelectionHandleInVisibleBound
45 import androidx.compose.foundation.text.selection.selectionGestureInput
46 import androidx.compose.foundation.text.selection.textFieldMagnifier
47 import androidx.compose.foundation.text.selection.updateSelectionTouchMode
48 import androidx.compose.runtime.Composable
49 import androidx.compose.runtime.DisposableEffect
50 import androidx.compose.runtime.DontMemoize
51 import androidx.compose.runtime.LaunchedEffect
52 import androidx.compose.runtime.MutableState
53 import androidx.compose.runtime.RecomposeScope
54 import androidx.compose.runtime.currentRecomposeScope
55 import androidx.compose.runtime.getValue
56 import androidx.compose.runtime.mutableStateOf
57 import androidx.compose.runtime.remember
58 import androidx.compose.runtime.rememberCoroutineScope
59 import androidx.compose.runtime.rememberUpdatedState
60 import androidx.compose.runtime.saveable.rememberSaveable
61 import androidx.compose.runtime.setValue
62 import androidx.compose.runtime.snapshotFlow
63 import androidx.compose.runtime.snapshots.Snapshot
64 import androidx.compose.ui.Modifier
65 import androidx.compose.ui.draw.drawBehind
66 import androidx.compose.ui.focus.FocusManager
67 import androidx.compose.ui.focus.FocusRequester
68 import androidx.compose.ui.geometry.Rect
69 import androidx.compose.ui.graphics.Brush
70 import androidx.compose.ui.graphics.Color
71 import androidx.compose.ui.graphics.Paint
72 import androidx.compose.ui.graphics.SolidColor
73 import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
74 import androidx.compose.ui.input.key.onPreviewKeyEvent
75 import androidx.compose.ui.input.pointer.PointerIcon
76 import androidx.compose.ui.input.pointer.pointerHoverIcon
77 import androidx.compose.ui.input.pointer.pointerInput
78 import androidx.compose.ui.layout.FirstBaseline
79 import androidx.compose.ui.layout.IntrinsicMeasurable
80 import androidx.compose.ui.layout.IntrinsicMeasureScope
81 import androidx.compose.ui.layout.LastBaseline
82 import androidx.compose.ui.layout.Layout
83 import androidx.compose.ui.layout.LayoutCoordinates
84 import androidx.compose.ui.layout.Measurable
85 import androidx.compose.ui.layout.MeasurePolicy
86 import androidx.compose.ui.layout.MeasureResult
87 import androidx.compose.ui.layout.MeasureScope
88 import androidx.compose.ui.layout.onGloballyPositioned
89 import androidx.compose.ui.platform.LocalClipboard
90 import androidx.compose.ui.platform.LocalDensity
91 import androidx.compose.ui.platform.LocalFocusManager
92 import androidx.compose.ui.platform.LocalFontFamilyResolver
93 import androidx.compose.ui.platform.LocalHapticFeedback
94 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
95 import androidx.compose.ui.platform.LocalTextToolbar
96 import androidx.compose.ui.platform.LocalWindowInfo
97 import androidx.compose.ui.platform.SoftwareKeyboardController
98 import androidx.compose.ui.semantics.semantics
99 import androidx.compose.ui.text.AnnotatedString
100 import androidx.compose.ui.text.TextLayoutResult
101 import androidx.compose.ui.text.TextRange
102 import androidx.compose.ui.text.TextStyle
103 import androidx.compose.ui.text.font.FontFamily
104 import androidx.compose.ui.text.input.EditProcessor
105 import androidx.compose.ui.text.input.ImeAction
106 import androidx.compose.ui.text.input.ImeOptions
107 import androidx.compose.ui.text.input.KeyboardType
108 import androidx.compose.ui.text.input.OffsetMapping
109 import androidx.compose.ui.text.input.PasswordVisualTransformation
110 import androidx.compose.ui.text.input.TextFieldValue
111 import androidx.compose.ui.text.input.TextInputService
112 import androidx.compose.ui.text.input.TextInputSession
113 import androidx.compose.ui.text.input.VisualTransformation
114 import androidx.compose.ui.unit.Constraints
115 import androidx.compose.ui.unit.Density
116 import androidx.compose.ui.unit.DpSize
117 import androidx.compose.ui.unit.dp
118 import androidx.compose.ui.util.fastRoundToInt
119 import kotlin.math.max
120 import kotlinx.coroutines.CoroutineScope
121 import kotlinx.coroutines.CoroutineStart
122 import kotlinx.coroutines.coroutineScope
123 import kotlinx.coroutines.launch
124 
125 /**
126  * Base composable that enables users to edit text via hardware or software keyboard.
127  *
128  * This composable provides basic text editing functionality, however does not include any
129  * decorations such as borders, hints/placeholder.
130  *
131  * If the editable text is larger than the size of the container, the vertical scrolling behaviour
132  * will be automatically applied. To enable a single line behaviour with horizontal scrolling
133  * instead, set the [maxLines] parameter to 1, [softWrap] to false, and [ImeOptions.singleLine] to
134  * true.
135  *
136  * Whenever the user edits the text, [onValueChange] is called with the most up to date state
137  * represented by [TextFieldValue]. [TextFieldValue] contains the text entered by user, as well as
138  * selection, cursor and text composition information. Please check [TextFieldValue] for the
139  * description of its contents.
140  *
141  * It is crucial that the value provided in the [onValueChange] is fed back into [CoreTextField] in
142  * order to have the final state of the text being displayed. Example usage:
143  *
144  * Please keep in mind that [onValueChange] is useful to be informed about the latest state of the
145  * text input by users, however it is generally not recommended to modify the values in the
146  * [TextFieldValue] that you get via [onValueChange] callback. Any change to the values in
147  * [TextFieldValue] may result in a context reset and end up with input session restart. Such a
148  * scenario would cause glitches in the UI or text input experience for users.
149  *
150  * @param value The [androidx.compose.ui.text.input.TextFieldValue] to be shown in the
151  *   [CoreTextField].
152  * @param onValueChange Called when the input service updates the values in [TextFieldValue].
153  * @param modifier optional [Modifier] for this text field.
154  * @param textStyle Style configuration that applies at character level such as color, font etc.
155  * @param visualTransformation The visual transformation filter for changing the visual
156  *   representation of the input. By default no visual transformation is applied.
157  * @param onTextLayout Callback that is executed when a new text layout is calculated. A
158  *   [TextLayoutResult] object that callback provides contains paragraph information, size of the
159  *   text, baselines and other details. The callback can be used to add additional decoration or
160  *   functionality to the text. For example, to draw a cursor or selection around the text.
161  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
162  *   for this CoreTextField. You can create and pass in your own remembered
163  *   [MutableInteractionSource] if you want to observe [Interaction]s and customize the appearance /
164  *   behavior of this CoreTextField in different [Interaction]s.
165  * @param cursorBrush [Brush] to paint cursor with. If [SolidColor] with [Color.Unspecified]
166  *   provided, there will be no cursor drawn
167  * @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the
168  *   text will be positioned as if there was unlimited horizontal space.
169  * @param maxLines The maximum height in terms of maximum number of visible lines. It is required
170  *   that 1 <= [minLines] <= [maxLines].
171  * @param minLines The minimum height in terms of minimum number of visible lines. It is required
172  *   that 1 <= [minLines] <= [maxLines].
173  * @param imeOptions Contains different IME configuration options.
174  * @param keyboardActions when the input service emits an IME action, the corresponding callback is
175  *   called. Note that this IME action may be different from what you specified in
176  *   [KeyboardOptions.imeAction].
177  * @param enabled controls the enabled state of the text field. When `false`, the text field will be
178  *   neither editable nor focusable, the input of the text field will not be selectable
179  * @param readOnly controls the editable state of the [CoreTextField]. When `true`, the text field
180  *   can not be modified, however, a user can focus it and copy text from it. Read-only text fields
181  *   are usually used to display pre-filled forms that user can not edit
182  * @param decorationBox Composable lambda that allows to add decorations around text field, such as
183  *   icon, placeholder, helper messages or similar, and automatically increase the hit target area
184  *   of the text field. To allow you to control the placement of the inner text field relative to
185  *   your decorations, the text field implementation will pass in a framework-controlled composable
186  *   parameter "innerTextField" to the decorationBox lambda you provide. You must call
187  *   innerTextField exactly once.
188  */
189 @Composable
190 internal fun CoreTextField(
191     value: TextFieldValue,
192     onValueChange: (TextFieldValue) -> Unit,
193     modifier: Modifier = Modifier,
194     textStyle: TextStyle = TextStyle.Default,
195     visualTransformation: VisualTransformation = VisualTransformation.None,
196     onTextLayout: (TextLayoutResult) -> Unit = {},
197     interactionSource: MutableInteractionSource? = null,
198     cursorBrush: Brush = SolidColor(Color.Unspecified),
199     softWrap: Boolean = true,
200     maxLines: Int = Int.MAX_VALUE,
201     minLines: Int = DefaultMinLines,
202     imeOptions: ImeOptions = ImeOptions.Default,
203     keyboardActions: KeyboardActions = KeyboardActions.Default,
204     enabled: Boolean = true,
205     readOnly: Boolean = false,
206     @Suppress("ComposableLambdaParameterPosition")
207     decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit =
208         @Composable { innerTextField -> innerTextField() },
209     textScrollerPosition: TextFieldScrollerPosition? = null,
210 ) {
<lambda>null211     val focusRequester = remember { FocusRequester() }
<lambda>null212     val legacyTextInputServiceAdapter = remember { createLegacyPlatformTextInputServiceAdapter() }
<lambda>null213     val textInputService: TextInputService = remember {
214         TextInputService(legacyTextInputServiceAdapter)
215     }
216 
217     // CompositionLocals
218     val density = LocalDensity.current
219     val fontFamilyResolver = LocalFontFamilyResolver.current
220     val selectionBackgroundColor = LocalTextSelectionColors.current.backgroundColor
221     val focusManager = LocalFocusManager.current
222     val windowInfo = LocalWindowInfo.current
223     val keyboardController = LocalSoftwareKeyboardController.current
224 
225     // Scroll state
226     val singleLine = maxLines == 1 && !softWrap && imeOptions.singleLine
227     val orientation = if (singleLine) Orientation.Horizontal else Orientation.Vertical
228     val scrollerPosition =
229         textScrollerPosition
<lambda>null230             ?: rememberSaveable(orientation, saver = TextFieldScrollerPosition.Saver) {
231                 TextFieldScrollerPosition(orientation)
232             }
233     if (scrollerPosition.orientation != orientation) {
234         throw IllegalArgumentException(
235             "Mismatching scroller orientation; " +
236                 (if (orientation == Orientation.Vertical)
237                     "only single-line, non-wrap text fields can scroll horizontally"
238                 else "single-line, non-wrap text fields can only scroll horizontally")
239         )
240     }
241 
242     // State
243     val transformedText =
<lambda>null244         remember(value, visualTransformation) {
245             val transformed = visualTransformation.filterWithValidation(value.annotatedString)
246 
247             value.composition?.let { TextFieldDelegate.applyCompositionDecoration(it, transformed) }
248                 ?: transformed
249         }
250 
251     val visualText = transformedText.text
252     val offsetMapping = transformedText.offsetMapping
253 
254     // If developer doesn't pass new value to TextField, recompose won't happen but internal state
255     // and IME may think it is updated. To fix this inconsistent state, enforce recompose.
256     val scope = currentRecomposeScope
257     val state =
<lambda>null258         remember(keyboardController) {
259             LegacyTextFieldState(
260                 TextDelegate(
261                     text = visualText,
262                     style = textStyle,
263                     softWrap = softWrap,
264                     density = density,
265                     fontFamilyResolver = fontFamilyResolver
266                 ),
267                 recomposeScope = scope,
268                 keyboardController = keyboardController
269             )
270         }
271     state.update(
272         value.annotatedString,
273         visualText,
274         textStyle,
275         softWrap,
276         density,
277         fontFamilyResolver,
278         onValueChange,
279         keyboardActions,
280         focusManager,
281         selectionBackgroundColor
282     )
283 
284     // notify the EditProcessor of value every recomposition
285     state.processor.reset(value, state.inputSession)
286 
<lambda>null287     val undoManager = remember { UndoManager() }
288     undoManager.snapshotIfNeeded(value)
289 
290     val coroutineScope = rememberCoroutineScope()
<lambda>null291     val bringIntoViewRequester = remember { BringIntoViewRequester() }
292 
<lambda>null293     val manager = remember { TextFieldSelectionManager(undoManager) }
294     manager.offsetMapping = offsetMapping
295     manager.visualTransformation = visualTransformation
296     manager.onValueChange = state.onValueChange
297     manager.state = state
298     manager.value = value
299     manager.clipboard = LocalClipboard.current
300     manager.coroutineScope = coroutineScope
301     manager.textToolbar = LocalTextToolbar.current
302     manager.hapticFeedBack = LocalHapticFeedback.current
303     manager.focusRequester = focusRequester
304     manager.editable = !readOnly
305     manager.enabled = enabled
306 
307     // Focus
308     val focusModifier =
309         Modifier.textFieldFocusModifier(
310             enabled = enabled,
311             focusRequester = focusRequester,
312             interactionSource = interactionSource
<lambda>null313         ) {
314             if (state.hasFocus == it.isFocused) {
315                 return@textFieldFocusModifier
316             }
317             state.hasFocus = it.isFocused
318 
319             if (state.hasFocus && enabled && !readOnly) {
320                 startInputSession(textInputService, state, value, imeOptions, offsetMapping)
321             } else {
322                 endInputSession(state)
323             }
324 
325             // The focusable modifier itself will request the entire focusable be brought into view
326             // when it gains focus – in this case, that's the decoration box. However, since text
327             // fields may have their own internal scrolling, and the decoration box can do anything,
328             // we also need to specifically request that the cursor itself be brought into view.
329             // TODO(b/216790855) If this request happens after the focusable's request, the field
330             //  will only be scrolled far enough to show the cursor, _not_ the entire decoration
331             //  box.
332             if (it.isFocused) {
333                 state.layoutResult?.let { layoutResult ->
334                     coroutineScope.launch {
335                         bringIntoViewRequester.bringSelectionEndIntoView(
336                             value,
337                             state.textDelegate,
338                             layoutResult.value,
339                             offsetMapping
340                         )
341                     }
342                 }
343             }
344             if (!it.isFocused) manager.deselect()
345         }
346 
347     // Hide the keyboard if made disabled or read-only while focused (b/237308379).
348     val writeable by rememberUpdatedState(enabled && !readOnly)
<lambda>null349     LaunchedEffect(Unit) {
350         try {
351             snapshotFlow { writeable }
352                 .collect { writeable ->
353                     // When hasFocus changes, the session will be stopped/started in the focus
354                     // handler so we don't need to handle its changes here.
355                     if (writeable && state.hasFocus) {
356                         startInputSession(
357                             textInputService,
358                             state,
359                             manager.value,
360                             imeOptions,
361                             manager.offsetMapping
362                         )
363                     } else {
364                         endInputSession(state)
365                     }
366                 }
367         } finally {
368             // TODO(b/230536793) This is a workaround since we don't get an explicit focus blur
369             //  event when the text field is removed from the composition entirely.
370             endInputSession(state)
371         }
372     }
373 
374     val pointerModifier =
<lambda>null375         Modifier.updateSelectionTouchMode { state.isInTouchMode = it }
offsetnull376             .tapPressTextFieldModifier(interactionSource, enabled) { offset ->
377                 tapToFocus(state, focusRequester, !readOnly)
378                 if (state.hasFocus && enabled) {
379                     if (state.handleState != HandleState.Selection) {
380                         state.layoutResult?.let { layoutResult ->
381                             TextFieldDelegate.setCursorOffset(
382                                 offset,
383                                 layoutResult,
384                                 state.processor,
385                                 offsetMapping,
386                                 state.onValueChange
387                             )
388                             // Won't enter cursor state when text is empty.
389                             if (state.textDelegate.text.isNotEmpty()) {
390                                 state.handleState = HandleState.Cursor
391                             }
392                         }
393                     } else {
394                         manager.deselect(offset)
395                     }
396                 }
397             }
398             .selectionGestureInput(
399                 mouseSelectionObserver = manager.mouseSelectionObserver,
400                 textDragObserver = manager.touchSelectionObserver,
401             )
402             .pointerHoverIcon(PointerIcon.Text)
403 
404     val drawModifier =
<lambda>null405         Modifier.drawBehind {
406             state.layoutResult?.let { layoutResult ->
407                 drawIntoCanvas { canvas ->
408                     TextFieldDelegate.draw(
409                         canvas,
410                         value,
411                         state.selectionPreviewHighlightRange,
412                         state.deletionPreviewHighlightRange,
413                         offsetMapping,
414                         layoutResult.value,
415                         state.highlightPaint,
416                         state.selectionBackgroundColor
417                     )
418                 }
419             }
420         }
421 
422     val onPositionedModifier =
<lambda>null423         Modifier.onGloballyPositioned {
424             state.layoutCoordinates = it
425             state.layoutResult?.innerTextFieldCoordinates = it
426             if (enabled) {
427                 if (state.handleState == HandleState.Selection) {
428                     if (state.showFloatingToolbar && windowInfo.isWindowFocused) {
429                         manager.showSelectionToolbar()
430                     } else {
431                         manager.hideSelectionToolbar()
432                     }
433                     state.showSelectionHandleStart =
434                         manager.isSelectionHandleInVisibleBound(isStartHandle = true)
435                     state.showSelectionHandleEnd =
436                         manager.isSelectionHandleInVisibleBound(isStartHandle = false)
437                     state.showCursorHandle = value.selection.collapsed
438                 } else if (state.handleState == HandleState.Cursor) {
439                     state.showCursorHandle =
440                         manager.isSelectionHandleInVisibleBound(isStartHandle = true)
441                 }
442                 notifyFocusedRect(state, value, offsetMapping)
443                 state.layoutResult?.let { layoutResult ->
444                     state.inputSession?.let { inputSession ->
445                         if (state.hasFocus) {
446                             TextFieldDelegate.updateTextLayoutResult(
447                                 inputSession,
448                                 value,
449                                 offsetMapping,
450                                 layoutResult
451                             )
452                         }
453                     }
454                 }
455             }
456         }
457 
458     val isPassword = visualTransformation is PasswordVisualTransformation
459     val semanticsModifier =
460         CoreTextFieldSemanticsModifier(
461             transformedText,
462             value,
463             state,
464             readOnly,
465             enabled,
466             isPassword,
467             offsetMapping,
468             manager,
469             imeOptions,
470             focusRequester
471         )
472 
473     val showCursor = enabled && !readOnly && windowInfo.isWindowFocused && !state.hasHighlight()
474     val cursorModifier = Modifier.cursor(state, value, offsetMapping, cursorBrush, showCursor)
475 
<lambda>null476     DisposableEffect(manager) { onDispose { manager.hideSelectionToolbar() } }
477 
<lambda>null478     DisposableEffect(imeOptions) {
479         if (state.hasFocus) {
480             state.inputSession =
481                 TextFieldDelegate.restartInput(
482                     textInputService = textInputService,
483                     value = value,
484                     editProcessor = state.processor,
485                     imeOptions = imeOptions,
486                     onValueChange = state.onValueChange,
487                     onImeActionPerformed = state.onImeActionPerformed
488                 )
489         }
490         onDispose { /* do nothing */ }
491     }
492 
493     val textKeyInputModifier =
494         Modifier.textFieldKeyInput(
495             state = state,
496             manager = manager,
497             value = value,
498             onValueChange = state.onValueChange,
499             editable = !readOnly,
500             singleLine = maxLines == 1,
501             offsetMapping = offsetMapping,
502             undoManager = undoManager,
503             imeAction = imeOptions.imeAction,
504         )
505 
506     val handwritingEnabled =
507         imeOptions.keyboardType != KeyboardType.Password &&
508             imeOptions.keyboardType != KeyboardType.NumberPassword
509     val stylusHandwritingModifier =
<lambda>null510         Modifier.stylusHandwriting(writeable, handwritingEnabled) {
511             // If this is a password field, we can't trigger handwriting.
512             // The expected behavior is 1) request focus 2) show software keyboard.
513             // Note: TextField will show software keyboard automatically when it
514             // gain focus. 3) show a toast message telling that handwriting is not
515             // supported for password fields. TODO(b/335294152)
516             if (handwritingEnabled) {
517                 // TextInputService is calling LegacyTextInputServiceAdapter under the
518                 // hood.  And because it's a public API, startStylusHandwriting is added
519                 // to legacyTextInputServiceAdapter instead.
520                 // startStylusHandwriting may be called before the actual input
521                 // session starts when the editor is not focused, this is handled
522                 // internally by the LegacyTextInputServiceAdapter.
523                 legacyTextInputServiceAdapter.startStylusHandwriting()
524             }
525         }
526 
527     val autofillHighlightColor = LocalAutofillHighlightColor.current
528     val drawDecorationModifier =
<lambda>null529         Modifier.drawBehind {
530             if (state.autofillHighlightOn || state.justAutofilled) {
531                 drawRect(color = autofillHighlightColor)
532             }
533         }
534 
535     // Modifiers that should be applied to the outer text field container. Usually those include
536     // gesture and semantics modifiers.
537     val decorationBoxModifier =
538         modifier
539             .then(drawDecorationModifier)
540             .legacyTextInputAdapter(legacyTextInputServiceAdapter, state, manager)
541             .then(stylusHandwritingModifier)
542             .then(focusModifier)
543             .interceptDPadAndMoveFocus(state, focusManager)
544             .previewKeyEventToDeselectOnBack(state, manager)
545             .then(textKeyInputModifier)
546             .textFieldScrollable(scrollerPosition, interactionSource, enabled)
547             .then(pointerModifier)
548             .then(semanticsModifier)
<lambda>null549             .onGloballyPositioned @DontMemoize { state.layoutResult?.decorationBoxCoordinates = it }
550             .addContextMenuComponents(manager, coroutineScope)
551 
552     val showHandleAndMagnifier =
553         enabled && state.hasFocus && state.isInTouchMode && windowInfo.isWindowFocused
554     val magnifierModifier =
555         if (showHandleAndMagnifier) {
556             Modifier.textFieldMagnifier(manager)
557         } else {
558             Modifier
559         }
560 
<lambda>null561     CoreTextFieldRootBox(decorationBoxModifier, manager) {
562         decorationBox {
563             // Modifiers applied directly to the internal input field implementation. In general,
564             // these will most likely include draw, layout and IME related modifiers.
565             val coreTextFieldModifier =
566                 Modifier
567                     // min height is set for maxLines == 1 in order to prevent text cuts for single
568                     // line
569                     // TextFields
570                     .heightIn(min = state.minHeightForSingleLineField)
571                     .heightInLines(textStyle = textStyle, minLines = minLines, maxLines = maxLines)
572                     .textFieldScroll(
573                         scrollerPosition = scrollerPosition,
574                         textFieldValue = value,
575                         visualTransformation = visualTransformation,
576                         textLayoutResultProvider = { state.layoutResult },
577                     )
578                     .then(cursorModifier)
579                     .then(drawModifier)
580                     .textFieldMinSize(textStyle)
581                     .then(onPositionedModifier)
582                     .then(magnifierModifier)
583                     .bringIntoViewRequester(bringIntoViewRequester)
584 
585             SimpleLayout(coreTextFieldModifier) {
586                 Layout(
587                     content = {},
588                     measurePolicy =
589                         object : MeasurePolicy {
590                             override fun MeasureScope.measure(
591                                 measurables: List<Measurable>,
592                                 constraints: Constraints
593                             ): MeasureResult {
594                                 val prevProxy =
595                                     Snapshot.withoutReadObservation { state.layoutResult }
596                                 val prevResult = prevProxy?.value
597                                 val (width, height, result) =
598                                     TextFieldDelegate.layout(
599                                         state.textDelegate,
600                                         constraints,
601                                         layoutDirection,
602                                         prevResult
603                                     )
604                                 if (prevResult != result) {
605                                     state.layoutResult =
606                                         TextLayoutResultProxy(
607                                             value = result,
608                                             decorationBoxCoordinates =
609                                                 prevProxy?.decorationBoxCoordinates,
610                                         )
611                                     onTextLayout(result)
612                                     notifyFocusedRect(state, value, offsetMapping)
613                                 }
614 
615                                 // calculate the min height for single line text to prevent text
616                                 // cuts.
617                                 // for single line text maxLines puts in max height constraint based
618                                 // on
619                                 // constant characters therefore if the user enters a character that
620                                 // is
621                                 // longer (i.e. emoji or a tall script) the text is cut
622                                 state.minHeightForSingleLineField =
623                                     with(density) {
624                                         when (maxLines) {
625                                             1 -> result.getLineBottom(0).ceilToIntPx()
626                                             else -> 0
627                                         }.toDp()
628                                     }
629 
630                                 return layout(
631                                     width = width,
632                                     height = height,
633                                     alignmentLines =
634                                         mapOf(
635                                             FirstBaseline to result.firstBaseline.fastRoundToInt(),
636                                             LastBaseline to result.lastBaseline.fastRoundToInt()
637                                         )
638                                 ) {}
639                             }
640 
641                             override fun IntrinsicMeasureScope.maxIntrinsicWidth(
642                                 measurables: List<IntrinsicMeasurable>,
643                                 height: Int
644                             ): Int {
645                                 state.textDelegate.layoutIntrinsics(layoutDirection)
646                                 return state.textDelegate.maxIntrinsicWidth
647                             }
648                         }
649                 )
650 
651                 SelectionToolbarAndHandles(
652                     manager = manager,
653                     show =
654                         state.handleState != HandleState.None &&
655                             state.layoutCoordinates != null &&
656                             state.layoutCoordinates!!.isAttached &&
657                             showHandleAndMagnifier
658                 )
659 
660                 if (
661                     state.handleState == HandleState.Cursor && !readOnly && showHandleAndMagnifier
662                 ) {
663                     TextFieldCursorHandle(manager = manager)
664                 }
665             }
666         }
667     }
668 }
669 
670 @Composable
CoreTextFieldRootBoxnull671 private fun CoreTextFieldRootBox(
672     modifier: Modifier,
673     manager: TextFieldSelectionManager,
674     content: @Composable () -> Unit
675 ) {
676     Box(modifier, propagateMinConstraints = true) { ContextMenuArea(manager, content) }
677 }
678 
679 /**
680  * The selection handle state of the TextField. It can be None, Selection or Cursor. It determines
681  * whether the selection handle, cursor handle or only cursor is shown. And how TextField handles
682  * gestures.
683  */
684 internal enum class HandleState {
685     /**
686      * No selection is active in this TextField. This is the initial state of the TextField. If the
687      * user long click on the text and start selection, the TextField will exit this state and
688      * enters [HandleState.Selection] state. If the user tap on the text, the TextField will exit
689      * this state and enters [HandleState.Cursor] state.
690      */
691     None,
692 
693     /**
694      * Selection handle is displayed for this TextField. User can drag the selection handle to
695      * change the selected text. If the user start editing the text, the TextField will exit this
696      * state and enters [HandleState.None] state. If the user tap on the text, the TextField will
697      * exit this state and enters [HandleState.Cursor] state.
698      */
699     Selection,
700 
701     /**
702      * Cursor handle is displayed for this TextField. User can drag the cursor handle to change the
703      * cursor position. If the user start editing the text, the TextField will exit this state and
704      * enters [HandleState.None] state. If the user long click on the text and start selection, the
705      * TextField will exit this state and enters [HandleState.Selection] state. Also notice that
706      * TextField won't enter this state if the current input text is empty.
707      */
708     Cursor
709 }
710 
711 /**
712  * Indicates which handle is being dragged when the user is dragging on a text field handle.
713  *
714  * @see LegacyTextFieldState.handleState
715  */
716 internal enum class Handle {
717     Cursor,
718     SelectionStart,
719     SelectionEnd
720 }
721 
722 /**
723  * Modifier to intercept back key presses, when supported by the platform, and deselect selected
724  * text and clear selection popups.
725  */
Modifiernull726 private fun Modifier.previewKeyEventToDeselectOnBack(
727     state: LegacyTextFieldState,
728     manager: TextFieldSelectionManager
729 ) = onPreviewKeyEvent { keyEvent ->
730     if (state.handleState == HandleState.Selection && keyEvent.cancelsTextSelection()) {
731         manager.deselect()
732         true
733     } else {
734         false
735     }
736 }
737 
738 internal class LegacyTextFieldState(
739     var textDelegate: TextDelegate,
740     val recomposeScope: RecomposeScope,
741     val keyboardController: SoftwareKeyboardController?,
742 ) {
743     val processor = EditProcessor()
744     var inputSession: TextInputSession? = null
745 
746     /**
747      * This should be a state as every time we update the value we need to redraw it. state
748      * observation during onDraw callback will make it work.
749      */
750     var hasFocus by mutableStateOf(false)
751 
752     /** Set to a non-zero value for single line TextFields in order to prevent text cuts. */
753     var minHeightForSingleLineField by mutableStateOf(0.dp)
754 
755     /**
756      * The last layout coordinates for the inner text field LayoutNode, used by selection and
757      * notifyFocusedRect. Since this layoutCoordinates only used for relative position calculation,
758      * we are guarding ourselves from using it when it's not attached.
759      */
760     private var _layoutCoordinates: LayoutCoordinates? = null
761     var layoutCoordinates: LayoutCoordinates?
<lambda>null762         get() = _layoutCoordinates?.takeIf { it.isAttached }
763         set(value) {
764             _layoutCoordinates = value
765         }
766 
767     /**
768      * You should be using proxy type [TextLayoutResultProxy] if you need to translate touch offset
769      * into text's coordinate system. For example, if you add a gesture on top of the decoration box
770      * and want to know the character in text for the given touch offset on decoration box. When you
771      * don't need to shift the touch offset, you should be using `layoutResult.value` which omits
772      * the proxy and calls the layout result directly. This is needed when you work with the text
773      * directly, and not the decoration box. For example, cursor modifier gets position using the
774      * [TextFieldValue.selection] value which corresponds to the text directly, and therefore does
775      * not require the translation.
776      */
777     private val layoutResultState: MutableState<TextLayoutResultProxy?> = mutableStateOf(null)
778     var layoutResult: TextLayoutResultProxy?
779         get() = layoutResultState.value
780         set(value) {
781             layoutResultState.value = value
782             isLayoutResultStale = false
783         }
784 
785     /**
786      * [textDelegate] keeps a reference to the visually transformed text that is visible to the
787      * user. TextFieldState needs to have access to the underlying value that is not transformed
788      * while making comparisons that test whether the user input actually changed.
789      *
790      * This field contains the real value that is passed by the user before it was visually
791      * transformed.
792      */
793     var untransformedText: AnnotatedString? = null
794 
795     /**
796      * The gesture detector state, to indicate whether current state is selection, cursor or
797      * editing.
798      *
799      * In the none state, no selection or cursor handle is shown, only the cursor is shown.
800      * TextField is initially in this state. To enter this state, input anything from the keyboard
801      * and modify the text.
802      *
803      * In the selection state, there is no cursor shown, only selection is shown. To enter the
804      * selection mode, just long press on the screen. In this mode, finger movement on the screen
805      * changes selection instead of moving the cursor.
806      *
807      * In the cursor state, no selection is shown, and the cursor and the cursor handle are shown.
808      * To enter the cursor state, tap anywhere within the TextField.(The TextField will stay in the
809      * edit state if the current text is empty.) In this mode, finger movement on the screen moves
810      * the cursor.
811      */
812     var handleState by mutableStateOf(HandleState.None)
813 
814     /**
815      * A flag to check if the floating toolbar should show.
816      *
817      * This state is meant to represent the floating toolbar status regardless of if all touch
818      * behaviors are disabled (like if the user is using a mouse). This is so that when touch
819      * behaviors are re-enabled, the toolbar status will still reflect whether it should be shown at
820      * that point.
821      */
822     var showFloatingToolbar by mutableStateOf(false)
823 
824     /**
825      * True if the position of the selection start handle is within a visible part of the window
826      * (i.e. not scrolled out of view) and the handle should be drawn.
827      */
828     var showSelectionHandleStart by mutableStateOf(false)
829 
830     /**
831      * True if the position of the selection end handle is within a visible part of the window (i.e.
832      * not scrolled out of view) and the handle should be drawn.
833      */
834     var showSelectionHandleEnd by mutableStateOf(false)
835 
836     /**
837      * True if the position of the cursor is within a visible part of the window (i.e. not scrolled
838      * out of view) and the handle should be drawn.
839      */
840     var showCursorHandle by mutableStateOf(false)
841 
842     /**
843      * TextFieldState holds both TextDelegate and layout result. However, these two values are not
844      * updated at the same time. TextDelegate is updated during composition according to new
845      * arguments while layoutResult is updated during layout phase. Therefore, [layoutResult] might
846      * not indicate the result of [textDelegate] at a given time during composition. This variable
847      * indicates whether layout result is lacking behind the latest TextDelegate.
848      */
849     var isLayoutResultStale: Boolean = true
850         private set
851 
852     var isInTouchMode: Boolean by mutableStateOf(true)
853 
854     private val keyboardActionRunner: KeyboardActionRunner =
855         KeyboardActionRunner(keyboardController)
856 
857     /** Autofill related values we need to save between */
858     var autofillHighlightOn by mutableStateOf(false)
859     var justAutofilled by mutableStateOf(false)
860 
861     /**
862      * DO NOT USE, use [onValueChange] instead. This is original callback provided to the TextField.
863      * In order the CoreTextField to work, the recompose.invalidate() has to be called when we call
864      * the callback and [onValueChange] is a wrapper that mainly does that.
865      */
<lambda>null866     private var onValueChangeOriginal: (TextFieldValue) -> Unit = {}
867 
<lambda>null868     val onValueChange: (TextFieldValue) -> Unit = {
869         if (it.text != untransformedText?.text) {
870             // Text has been changed, enter the HandleState.None and hide the cursor handle.
871             handleState = HandleState.None
872 
873             // Autofill logic
874             if (justAutofilled) {
875                 justAutofilled = false
876             } else {
877                 autofillHighlightOn = false
878             }
879         }
880         selectionPreviewHighlightRange = TextRange.Zero
881         deletionPreviewHighlightRange = TextRange.Zero
882         onValueChangeOriginal(it)
883         recomposeScope.invalidate()
884     }
885 
imeActionnull886     val onImeActionPerformed: (ImeAction) -> Unit = { imeAction ->
887         keyboardActionRunner.runAction(imeAction)
888     }
imeActionnull889     val onImeActionPerformedWithResult: (ImeAction) -> Boolean = { imeAction ->
890         keyboardActionRunner.runAction(imeAction)
891     }
892 
893     /** The paint used to draw highlight backgrounds. */
894     val highlightPaint: Paint = Paint()
895     var selectionBackgroundColor = Color.Unspecified
896 
897     /** Range of text to be highlighted to display handwriting gesture previews from the IME. */
898     var selectionPreviewHighlightRange: TextRange by mutableStateOf(TextRange.Zero)
899     var deletionPreviewHighlightRange: TextRange by mutableStateOf(TextRange.Zero)
900 
hasHighlightnull901     fun hasHighlight() =
902         !selectionPreviewHighlightRange.collapsed || !deletionPreviewHighlightRange.collapsed
903 
904     fun update(
905         untransformedText: AnnotatedString,
906         visualText: AnnotatedString,
907         textStyle: TextStyle,
908         softWrap: Boolean,
909         density: Density,
910         fontFamilyResolver: FontFamily.Resolver,
911         onValueChange: (TextFieldValue) -> Unit,
912         keyboardActions: KeyboardActions,
913         focusManager: FocusManager,
914         selectionBackgroundColor: Color
915     ) {
916         this.onValueChangeOriginal = onValueChange
917         this.selectionBackgroundColor = selectionBackgroundColor
918         this.keyboardActionRunner.apply {
919             this.keyboardActions = keyboardActions
920             this.focusManager = focusManager
921         }
922         this.untransformedText = untransformedText
923 
924         val newTextDelegate =
925             updateTextDelegate(
926                 current = textDelegate,
927                 text = visualText,
928                 style = textStyle,
929                 softWrap = softWrap,
930                 density = density,
931                 fontFamilyResolver = fontFamilyResolver,
932                 placeholders = emptyList(),
933             )
934 
935         if (textDelegate !== newTextDelegate) isLayoutResultStale = true
936         textDelegate = newTextDelegate
937     }
938 }
939 
940 /** Request focus on tap. If already focused, makes sure the keyboard is requested. */
tapToFocusnull941 internal fun tapToFocus(
942     state: LegacyTextFieldState,
943     focusRequester: FocusRequester,
944     allowKeyboard: Boolean
945 ) {
946     if (!state.hasFocus) {
947         focusRequester.requestFocus()
948     } else if (allowKeyboard) {
949         state.keyboardController?.show()
950     }
951 }
952 
startInputSessionnull953 private fun startInputSession(
954     textInputService: TextInputService,
955     state: LegacyTextFieldState,
956     value: TextFieldValue,
957     imeOptions: ImeOptions,
958     offsetMapping: OffsetMapping
959 ) {
960     state.inputSession =
961         TextFieldDelegate.onFocus(
962             textInputService,
963             value,
964             state.processor,
965             imeOptions,
966             state.onValueChange,
967             state.onImeActionPerformed
968         )
969     notifyFocusedRect(state, value, offsetMapping)
970 }
971 
endInputSessionnull972 private fun endInputSession(state: LegacyTextFieldState) {
973     state.inputSession?.let { session ->
974         TextFieldDelegate.onBlur(session, state.processor, state.onValueChange)
975     }
976     state.inputSession = null
977 }
978 
979 /**
980  * Calculates the location of the end of the current selection and requests that it be brought into
981  * view using [bringCursorIntoView][BringIntoViewRequester.bringIntoView].
982  *
983  * Text fields have a lot of different edge cases where they need to make sure they stay visible:
984  * 1. Focusable node newly receives focus – always bring entire node into view.
985  * 2. Unfocused text field is tapped – always bring cursor area into view (conflicts with above, see
986  *    b/216790855).
987  * 3. Focused text field is tapped – always bring cursor area into view.
988  * 4. Text input occurs – always bring cursor area into view.
989  * 5. Scrollable parent resizes and the currently-focused item is now hidden – bring entire node
990  *    into view if it was also in view before the resize. This handles the case of
991  *    `softInputMode=ADJUST_RESIZE`. See b/216842427.
992  * 6. Entire window is panned due to `softInputMode=ADJUST_PAN` – report the correct focused rect to
993  *    the view system, and the view system itself will keep the focused area in view. See
994  *    aosp/1964580.
995  *
996  * This function is used to handle 2, 3, and 4, and the others are automatically handled by the
997  * focus system.
998  */
bringSelectionEndIntoViewnull999 internal suspend fun BringIntoViewRequester.bringSelectionEndIntoView(
1000     value: TextFieldValue,
1001     textDelegate: TextDelegate,
1002     textLayoutResult: TextLayoutResult,
1003     offsetMapping: OffsetMapping
1004 ) {
1005     val selectionEndInTransformed = offsetMapping.originalToTransformed(value.selection.max)
1006     val selectionEndBounds =
1007         when {
1008             selectionEndInTransformed < textLayoutResult.layoutInput.text.length -> {
1009                 textLayoutResult.getBoundingBox(selectionEndInTransformed)
1010             }
1011             selectionEndInTransformed != 0 -> {
1012                 textLayoutResult.getBoundingBox(selectionEndInTransformed - 1)
1013             }
1014             else -> { // empty text.
1015                 val defaultSize =
1016                     computeSizeForDefaultText(
1017                         textDelegate.style,
1018                         textDelegate.density,
1019                         textDelegate.fontFamilyResolver
1020                     )
1021                 Rect(0f, 0f, 1.0f, defaultSize.height.toFloat())
1022             }
1023         }
1024     bringIntoView(selectionEndBounds)
1025 }
1026 
1027 @Composable
SelectionToolbarAndHandlesnull1028 private fun SelectionToolbarAndHandles(manager: TextFieldSelectionManager, show: Boolean) {
1029     with(manager) {
1030         if (show) {
1031             // Check whether text layout result became stale. A stale text layout might be
1032             // completely unrelated to current TextFieldValue, causing offset errors.
1033             state
1034                 ?.layoutResult
1035                 ?.value
1036                 ?.takeIf { !(state?.isLayoutResultStale ?: true) }
1037                 ?.let {
1038                     if (!value.selection.collapsed) {
1039                         val startOffset = offsetMapping.originalToTransformed(value.selection.start)
1040                         val endOffset = offsetMapping.originalToTransformed(value.selection.end)
1041                         val startDirection = it.getBidiRunDirection(startOffset)
1042                         val endDirection = it.getBidiRunDirection(max(endOffset - 1, 0))
1043                         if (manager.state?.showSelectionHandleStart == true) {
1044                             TextFieldSelectionHandle(
1045                                 isStartHandle = true,
1046                                 direction = startDirection,
1047                                 manager = manager
1048                             )
1049                         }
1050                         if (manager.state?.showSelectionHandleEnd == true) {
1051                             TextFieldSelectionHandle(
1052                                 isStartHandle = false,
1053                                 direction = endDirection,
1054                                 manager = manager
1055                             )
1056                         }
1057                     }
1058 
1059                     state?.let { textFieldState ->
1060                         // If in selection mode (when the floating toolbar is shown) a new symbol
1061                         // from the keyboard is entered, text field should enter the editing mode
1062                         // instead.
1063                         if (isTextChanged()) textFieldState.showFloatingToolbar = false
1064                         if (textFieldState.hasFocus) {
1065                             if (textFieldState.showFloatingToolbar) showSelectionToolbar()
1066                             else hideSelectionToolbar()
1067                         }
1068                     }
1069                 }
1070         } else hideSelectionToolbar()
1071     }
1072 }
1073 
1074 @Composable
TextFieldCursorHandlenull1075 internal fun TextFieldCursorHandle(manager: TextFieldSelectionManager) {
1076     if (manager.state?.showCursorHandle == true && manager.transformedText?.isNotEmpty() == true) {
1077         val observer = remember(manager) { manager.cursorDragObserver() }
1078         val position = manager.getCursorPosition(LocalDensity.current)
1079         CursorHandle(
1080             offsetProvider = { position },
1081             modifier =
1082                 Modifier.pointerInput(observer) {
1083                         coroutineScope {
1084                             // UNDISPATCHED because this runs upon first pointer event and
1085                             // without it the event would pass before the handler is ready
1086                             launch(start = CoroutineStart.UNDISPATCHED) {
1087                                 detectDownAndDragGesturesWithObserver(observer)
1088                             }
1089                             launch(start = CoroutineStart.UNDISPATCHED) {
1090                                 detectTapGestures { manager.showSelectionToolbar() }
1091                             }
1092                         }
1093                     }
1094                     .semantics {
1095                         this[SelectionHandleInfoKey] =
1096                             SelectionHandleInfo(
1097                                 handle = Handle.Cursor,
1098                                 position = position,
1099                                 anchor = SelectionHandleAnchor.Middle,
1100                                 visible = true,
1101                             )
1102                     }
1103         )
1104     }
1105 }
1106 
1107 @Composable
CursorHandlenull1108 internal expect fun CursorHandle(
1109     offsetProvider: OffsetProvider,
1110     modifier: Modifier,
1111     minTouchTargetSize: DpSize = DpSize.Unspecified
1112 )
1113 
1114 // TODO(b/262648050) Try to find a better API.
1115 private fun notifyFocusedRect(
1116     state: LegacyTextFieldState,
1117     value: TextFieldValue,
1118     offsetMapping: OffsetMapping
1119 ) {
1120     // If this reports state reads it causes an invalidation cycle.
1121     // This function doesn't need to be invalidated anyway because it's already explicitly called
1122     // after updating text layout or position.
1123     Snapshot.withoutReadObservation {
1124         val layoutResult = state.layoutResult ?: return
1125         val inputSession = state.inputSession ?: return
1126         val layoutCoordinates = state.layoutCoordinates ?: return
1127         TextFieldDelegate.notifyFocusedRect(
1128             value,
1129             state.textDelegate,
1130             layoutResult.value,
1131             layoutCoordinates,
1132             inputSession,
1133             state.hasFocus,
1134             offsetMapping
1135         )
1136     }
1137 }
1138 
1139 @OptIn(ExperimentalFoundationApi::class)
addContextMenuComponentsnull1140 private fun Modifier.addContextMenuComponents(
1141     textFieldSelectionManager: TextFieldSelectionManager,
1142     coroutineScope: CoroutineScope
1143 ): Modifier =
1144     if (ComposeFoundationFlags.isNewContextMenuEnabled)
1145         addBasicTextFieldTextContextMenuComponents(textFieldSelectionManager, coroutineScope)
1146     else this
1147