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