| /* |
| * Copyright 2023 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.compose.foundation.text2 |
| |
| import androidx.compose.foundation.ExperimentalFoundationApi |
| import androidx.compose.foundation.ScrollState |
| import androidx.compose.foundation.focusable |
| import androidx.compose.foundation.gestures.Orientation |
| import androidx.compose.foundation.gestures.ScrollableDefaults |
| import androidx.compose.foundation.gestures.scrollable |
| import androidx.compose.foundation.interaction.Interaction |
| import androidx.compose.foundation.interaction.MutableInteractionSource |
| import androidx.compose.foundation.interaction.collectIsFocusedAsState |
| import androidx.compose.foundation.layout.Box |
| import androidx.compose.foundation.layout.heightIn |
| import androidx.compose.foundation.rememberScrollState |
| import androidx.compose.foundation.text.CursorHandle |
| import androidx.compose.foundation.text.Handle |
| import androidx.compose.foundation.text.KeyboardActions |
| import androidx.compose.foundation.text.KeyboardOptions |
| import androidx.compose.foundation.text.heightInLines |
| import androidx.compose.foundation.text.selection.SelectionHandleAnchor |
| import androidx.compose.foundation.text.selection.SelectionHandleInfo |
| import androidx.compose.foundation.text.selection.SelectionHandleInfoKey |
| import androidx.compose.foundation.text.textFieldMinSize |
| import androidx.compose.foundation.text2.input.CodepointTransformation |
| import androidx.compose.foundation.text2.input.InputTransformation |
| import androidx.compose.foundation.text2.input.SingleLineCodepointTransformation |
| import androidx.compose.foundation.text2.input.TextFieldLineLimits |
| import androidx.compose.foundation.text2.input.TextFieldLineLimits.MultiLine |
| import androidx.compose.foundation.text2.input.TextFieldLineLimits.SingleLine |
| import androidx.compose.foundation.text2.input.TextFieldState |
| import androidx.compose.foundation.text2.input.internal.TextFieldCoreModifier |
| import androidx.compose.foundation.text2.input.internal.TextFieldDecoratorModifier |
| import androidx.compose.foundation.text2.input.internal.TextFieldTextLayoutModifier |
| import androidx.compose.foundation.text2.input.internal.TextLayoutState |
| import androidx.compose.foundation.text2.input.internal.TransformedTextFieldState |
| import androidx.compose.foundation.text2.input.internal.selection.TextFieldSelectionHandle2 |
| import androidx.compose.foundation.text2.input.internal.selection.TextFieldSelectionState |
| import androidx.compose.foundation.text2.input.internal.syncTextFieldState |
| import androidx.compose.runtime.Composable |
| import androidx.compose.runtime.DisposableEffect |
| import androidx.compose.runtime.SideEffect |
| import androidx.compose.runtime.getValue |
| import androidx.compose.runtime.mutableStateOf |
| import androidx.compose.runtime.remember |
| import androidx.compose.runtime.setValue |
| import androidx.compose.ui.Modifier |
| import androidx.compose.ui.draw.clipToBounds |
| import androidx.compose.ui.graphics.Brush |
| import androidx.compose.ui.graphics.Color |
| import androidx.compose.ui.graphics.SolidColor |
| import androidx.compose.ui.input.pointer.pointerInput |
| import androidx.compose.ui.platform.LocalClipboardManager |
| import androidx.compose.ui.platform.LocalDensity |
| import androidx.compose.ui.platform.LocalHapticFeedback |
| import androidx.compose.ui.platform.LocalLayoutDirection |
| import androidx.compose.ui.platform.LocalTextToolbar |
| import androidx.compose.ui.platform.LocalWindowInfo |
| import androidx.compose.ui.semantics.semantics |
| import androidx.compose.ui.text.TextLayoutResult |
| import androidx.compose.ui.text.TextRange |
| import androidx.compose.ui.text.TextStyle |
| import androidx.compose.ui.text.input.ImeAction |
| import androidx.compose.ui.text.input.KeyboardType |
| import androidx.compose.ui.text.input.TextFieldValue |
| import androidx.compose.ui.unit.Density |
| |
| /** |
| * BasicTextField2 is a new text input Composable under heavy development. Please refrain from |
| * using it in production since it has a very unstable API and implementation for the time being. |
| * Many core features like selection, cursor, gestures, etc. may fail or simply not exist. |
| * |
| * Basic text composable that provides an interactive box that accepts text input through software |
| * or hardware keyboard. |
| * |
| * Whenever the user edits the text, [onValueChange] is called with the most up to date state |
| * represented by [String] with which developer is expected to update their state. |
| * |
| * While focused and being edited, the caller temporarily loses _direct_ control of the contents of |
| * the field through the [value] parameter. If an unexpected [value] is passed in during this time, |
| * the contents of the field will _not_ be updated to reflect the value until editing is done. When |
| * editing is done (i.e. focus is lost), the field will be updated to the last [value] received. Use |
| * a [inputTransformation] to accept or reject changes during editing. For more direct control of |
| * the field contents use the [BasicTextField2] overload that accepts a [TextFieldState]. |
| * |
| * Unlike [TextFieldValue] overload, this composable does not let the developer control selection, |
| * cursor, and text composition information. Please check [TextFieldValue] and corresponding |
| * [BasicTextField2] overload for more information. |
| * |
| * If you want to add decorations to your text field, such as icon or similar, and increase the |
| * hit target area, use the decorator: |
| * @sample androidx.compose.foundation.samples.BasicTextField2DecoratorSample |
| * |
| * @param value The input [String] text to be shown in the text field. |
| * @param onValueChange The callback that is triggered when the user or the system updates the |
| * text. The updated text is passed as a parameter of the callback. The value passed to the callback |
| * will already have had the [inputTransformation] applied. |
| * @param modifier optional [Modifier] for this text field. |
| * @param enabled controls the enabled state of the [BasicTextField2]. When `false`, the text |
| * field will be neither editable nor focusable, the input of the text field will not be selectable. |
| * @param readOnly controls the editable state of the [BasicTextField2]. When `true`, the text |
| * field can not be modified, however, a user can focus it and copy text from it. Read-only text |
| * fields are usually used to display pre-filled forms that user can not edit. |
| * @param inputTransformation Optional [InputTransformation] that will be used to filter changes to |
| * the [TextFieldState] made by the user. The filter will be applied to changes made by hardware and |
| * software keyboard events, pasting or dropping text, accessibility services, and tests. The filter |
| * will _not_ be applied when a new [value] is passe din, or when the filter is changed. |
| * If the filter is changed on an existing text field, it will be applied to the next user edit, it |
| * will not immediately affect the current state. |
| * @param textStyle Typographic and graphic style configuration for text content that's displayed |
| * in the editor. |
| * @param keyboardOptions Software keyboard options that contain configurations such as |
| * [KeyboardType] and [ImeAction]. |
| * @param keyboardActions When the input service emits an IME action, the corresponding callback |
| * is called. Note that this IME action may be different from what you specified in |
| * [KeyboardOptions.imeAction]. |
| * @param lineLimits Whether the text field should be [SingleLine], scroll horizontally, and |
| * ignore newlines; or [MultiLine] and grow and scroll vertically. If [SingleLine] is passed without |
| * specifying the [codepointTransformation] parameter, a [CodepointTransformation] is automatically |
| * applied. This transformation replaces any newline characters ('\n') within the text with regular |
| * whitespace (' '), ensuring that the contents of the text field are presented in a single line. |
| * @param onTextLayout Callback that is executed when a new text layout is calculated. A |
| * [TextLayoutResult] object contains paragraph information, size of the text, baselines and other |
| * details. The callback can be used to add additional decoration or functionality to the text. |
| * For example, to draw a cursor or selection around the text. [Density] scope is the one that was |
| * used while creating the given text layout. |
| * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s |
| * for this TextField. You can create and pass in your own remembered [MutableInteractionSource] |
| * if you want to observe [Interaction]s and customize the appearance / behavior of this TextField |
| * for different [Interaction]s. |
| * @param cursorBrush [Brush] to paint cursor with. If [SolidColor] with [Color.Unspecified] |
| * provided, then no cursor will be drawn. |
| * @param codepointTransformation Visual transformation interface that provides a 1-to-1 mapping of |
| * codepoints. |
| * @param decorator Allows to add decorations around text field, such as icon, placeholder, helper |
| * messages or similar, and automatically increase the hit target area of the text field. |
| * @param scrollState Scroll state that manages either horizontal or vertical scroll of TextField. |
| * If [lineLimits] is [SingleLine], this text field is treated as single line with horizontal |
| * scroll behavior. In other cases the text field becomes vertically scrollable. |
| */ |
| @ExperimentalFoundationApi |
| // This takes a composable lambda, but it is not primarily a container. |
| @Suppress("ComposableLambdaParameterPosition") |
| @Composable |
| fun BasicTextField2( |
| value: String, |
| onValueChange: (String) -> Unit, |
| modifier: Modifier = Modifier, |
| enabled: Boolean = true, |
| readOnly: Boolean = false, |
| inputTransformation: InputTransformation? = null, |
| textStyle: TextStyle = TextStyle.Default, |
| keyboardOptions: KeyboardOptions = KeyboardOptions.Default, |
| keyboardActions: KeyboardActions = KeyboardActions.Default, |
| lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default, |
| onTextLayout: Density.(getResult: () -> TextLayoutResult?) -> Unit = {}, |
| interactionSource: MutableInteractionSource? = null, |
| cursorBrush: Brush = SolidColor(Color.Black), |
| codepointTransformation: CodepointTransformation? = null, |
| decorator: TextFieldDecorator? = null, |
| scrollState: ScrollState = rememberScrollState(), |
| // Last parameter must not be a function unless it's intended to be commonly used as a trailing |
| // lambda. |
| ) { |
| val state = remember { |
| TextFieldState( |
| initialText = value, |
| // Initialize the cursor to be at the end of the field. |
| initialSelectionInChars = TextRange(value.length) |
| ) |
| } |
| |
| // This is effectively a rememberUpdatedState, but it combines the updated state (text) with |
| // some state that is preserved across updates (selection). |
| var valueWithSelection by remember { |
| mutableStateOf( |
| TextFieldValue( |
| text = value, |
| selection = TextRange(value.length) |
| ) |
| ) |
| } |
| valueWithSelection = valueWithSelection.copy(text = value) |
| |
| BasicTextField2( |
| state = state, |
| modifier = modifier.syncTextFieldState( |
| state = state, |
| value = valueWithSelection, |
| onValueChanged = { |
| // Don't fire the callback if only the selection/cursor changed. |
| if (it.text != valueWithSelection.text) { |
| onValueChange(it.text) |
| } |
| valueWithSelection = it |
| }, |
| writeSelectionFromTextFieldValue = false |
| ), |
| enabled = enabled, |
| readOnly = readOnly, |
| inputTransformation = inputTransformation, |
| textStyle = textStyle, |
| keyboardOptions = keyboardOptions, |
| keyboardActions = keyboardActions, |
| lineLimits = lineLimits, |
| onTextLayout = onTextLayout, |
| interactionSource = interactionSource, |
| cursorBrush = cursorBrush, |
| scrollState = scrollState, |
| codepointTransformation = codepointTransformation, |
| decorator = decorator, |
| ) |
| } |
| |
| /** |
| * BasicTextField2 is a new text input Composable under heavy development. Please refrain from |
| * using it in production since it has a very unstable API and implementation for the time being. |
| * Many core features like selection, cursor, gestures, etc. may fail or simply not exist. |
| * |
| * Basic text composable that provides an interactive box that accepts text input through software |
| * or hardware keyboard. |
| * |
| * All the editing state of this composable is hoisted through [state]. Whenever the contents of |
| * this composable change via user input or semantics, [TextFieldState.text] gets updated. |
| * Similarly, all the programmatic updates made to [state] also reflect on this composable. |
| * |
| * If you want to add decorations to your text field, such as icon or similar, and increase the |
| * hit target area, use the decorator: |
| * @sample androidx.compose.foundation.samples.BasicTextField2DecoratorSample |
| * |
| * @param state [TextFieldState] object that holds the internal editing state of [BasicTextField2]. |
| * @param modifier optional [Modifier] for this text field. |
| * @param enabled controls the enabled state of the [BasicTextField2]. When `false`, the text |
| * field will be neither editable nor focusable, the input of the text field will not be selectable. |
| * @param readOnly controls the editable state of the [BasicTextField2]. When `true`, the text |
| * field can not be modified, however, a user can focus it and copy text from it. Read-only text |
| * fields are usually used to display pre-filled forms that user can not edit. |
| * @param inputTransformation Optional [InputTransformation] that will be used to filter changes to |
| * the [TextFieldState] made by the user. The filter will be applied to changes made by hardware and |
| * software keyboard events, pasting or dropping text, accessibility services, and tests. The filter |
| * will _not_ be applied when changing the [state] programmatically, or when the filter is changed. |
| * If the filter is changed on an existing text field, it will be applied to the next user edit. |
| * the filter will not immediately affect the current [state]. |
| * @param textStyle Typographic and graphic style configuration for text content that's displayed |
| * in the editor. |
| * @param keyboardOptions Software keyboard options that contain configurations such as |
| * [KeyboardType] and [ImeAction]. |
| * @param keyboardActions When the input service emits an IME action, the corresponding callback |
| * is called. Note that this IME action may be different from what you specified in |
| * [KeyboardOptions.imeAction]. |
| * @param lineLimits Whether the text field should be [SingleLine], scroll horizontally, and |
| * ignore newlines; or [MultiLine] and grow and scroll vertically. If [SingleLine] is passed without |
| * specifying the [codepointTransformation] parameter, a [CodepointTransformation] is automatically |
| * applied. This transformation replaces any newline characters ('\n') within the text with regular |
| * whitespace (' '), ensuring that the contents of the text field are presented in a single line. |
| * @param onTextLayout Callback that is executed when a new text layout is calculated. A |
| * [TextLayoutResult] object contains paragraph information, size of the text, baselines and other |
| * details. The callback can be used to add additional decoration or functionality to the text. |
| * For example, to draw a cursor or selection around the text. [Density] scope is the one that was |
| * used while creating the given text layout. |
| * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s |
| * for this TextField. You can create and pass in your own remembered [MutableInteractionSource] |
| * if you want to observe [Interaction]s and customize the appearance / behavior of this TextField |
| * for different [Interaction]s. |
| * @param cursorBrush [Brush] to paint cursor with. If [SolidColor] with [Color.Unspecified] |
| * provided, then no cursor will be drawn. |
| * @param codepointTransformation Visual transformation interface that provides a 1-to-1 mapping of |
| * codepoints. |
| * @param decorator Allows to add decorations around text field, such as icon, placeholder, helper |
| * messages or similar, and automatically increase the hit target area of the text field. |
| * @param scrollState Scroll state that manages either horizontal or vertical scroll of TextField. |
| * If [lineLimits] is [SingleLine], this text field is treated as single line with horizontal |
| * scroll behavior. In other cases the text field becomes vertically scrollable. |
| */ |
| @ExperimentalFoundationApi |
| // This takes a composable lambda, but it is not primarily a container. |
| @Suppress("ComposableLambdaParameterPosition") |
| @Composable |
| fun BasicTextField2( |
| state: TextFieldState, |
| modifier: Modifier = Modifier, |
| enabled: Boolean = true, |
| readOnly: Boolean = false, |
| inputTransformation: InputTransformation? = null, |
| textStyle: TextStyle = TextStyle.Default, |
| keyboardOptions: KeyboardOptions = KeyboardOptions.Default, |
| keyboardActions: KeyboardActions = KeyboardActions.Default, |
| lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default, |
| onTextLayout: Density.(getResult: () -> TextLayoutResult?) -> Unit = {}, |
| interactionSource: MutableInteractionSource? = null, |
| cursorBrush: Brush = SolidColor(Color.Black), |
| codepointTransformation: CodepointTransformation? = null, |
| decorator: TextFieldDecorator? = null, |
| scrollState: ScrollState = rememberScrollState(), |
| // Last parameter must not be a function unless it's intended to be commonly used as a trailing |
| // lambda. |
| ) { |
| val density = LocalDensity.current |
| val layoutDirection = LocalLayoutDirection.current |
| val windowInfo = LocalWindowInfo.current |
| val singleLine = lineLimits == SingleLine |
| // We're using this to communicate focus state to cursor for now. |
| @Suppress("NAME_SHADOWING") |
| val interactionSource = interactionSource ?: remember { MutableInteractionSource() } |
| val orientation = if (singleLine) Orientation.Horizontal else Orientation.Vertical |
| val isFocused = interactionSource.collectIsFocusedAsState().value |
| val isWindowFocused = windowInfo.isWindowFocused |
| |
| val transformedState = remember(state, inputTransformation, codepointTransformation) { |
| // First prefer provided codepointTransformation if not null, e.g. BasicSecureTextField |
| // would send PasswordTransformation. Second, apply a SingleLineCodepointTransformation if |
| // text field is configured to be single line. Else, don't apply any visual transformation. |
| val appliedCodepointTransformation = codepointTransformation |
| ?: SingleLineCodepointTransformation.takeIf { singleLine } |
| TransformedTextFieldState(state, inputTransformation, appliedCodepointTransformation) |
| } |
| |
| // Invalidate textLayoutState if TextFieldState itself has changed, since TextLayoutState |
| // would be carrying an invalid TextFieldState in its nonMeasureInputs. |
| val textLayoutState = remember(transformedState) { TextLayoutState() } |
| |
| val textFieldSelectionState = remember(transformedState) { |
| TextFieldSelectionState( |
| textFieldState = transformedState, |
| textLayoutState = textLayoutState, |
| density = density, |
| enabled = enabled, |
| readOnly = readOnly, |
| isFocused = isFocused && isWindowFocused |
| ) |
| } |
| val currentHapticFeedback = LocalHapticFeedback.current |
| val currentClipboardManager = LocalClipboardManager.current |
| val currentTextToolbar = LocalTextToolbar.current |
| SideEffect { |
| // These properties are not backed by snapshot state, so they can't be updated directly in |
| // composition. |
| textFieldSelectionState.update( |
| hapticFeedBack = currentHapticFeedback, |
| clipboardManager = currentClipboardManager, |
| textToolbar = currentTextToolbar, |
| density = density, |
| enabled = enabled, |
| readOnly = readOnly, |
| ) |
| } |
| |
| DisposableEffect(textFieldSelectionState) { |
| onDispose { |
| textFieldSelectionState.dispose() |
| } |
| } |
| |
| val decorationModifiers = modifier |
| .then( |
| // semantics + some focus + input session + touch to focus |
| TextFieldDecoratorModifier( |
| textFieldState = transformedState, |
| textLayoutState = textLayoutState, |
| textFieldSelectionState = textFieldSelectionState, |
| filter = inputTransformation, |
| enabled = enabled, |
| readOnly = readOnly, |
| keyboardOptions = keyboardOptions, |
| keyboardActions = keyboardActions, |
| singleLine = singleLine, |
| ) |
| ) |
| .focusable(interactionSource = interactionSource, enabled = enabled) |
| .scrollable( |
| state = scrollState, |
| orientation = orientation, |
| // Disable scrolling when textField is disabled, there is no where to scroll, and |
| // another dragging gesture is taking place |
| enabled = enabled && |
| scrollState.maxValue > 0 && |
| textFieldSelectionState.draggingHandle == null, |
| reverseDirection = ScrollableDefaults.reverseDirection( |
| layoutDirection = layoutDirection, |
| orientation = orientation, |
| reverseScrolling = false |
| ), |
| interactionSource = interactionSource, |
| ) |
| |
| Box(decorationModifiers, propagateMinConstraints = true) { |
| val nonNullDecorator = decorator ?: DefaultTextFieldDecorator |
| nonNullDecorator.Decoration { |
| val minLines: Int |
| val maxLines: Int |
| if (lineLimits is MultiLine) { |
| minLines = lineLimits.minHeightInLines |
| maxLines = lineLimits.maxHeightInLines |
| } else { |
| minLines = 1 |
| maxLines = 1 |
| } |
| |
| Box( |
| propagateMinConstraints = true, |
| modifier = Modifier |
| .heightIn(min = textLayoutState.minHeightForSingleLineField) |
| .heightInLines( |
| textStyle = textStyle, |
| minLines = minLines, |
| maxLines = maxLines |
| ) |
| .textFieldMinSize(textStyle) |
| .clipToBounds() |
| .then( |
| TextFieldCoreModifier( |
| isFocused = isFocused && isWindowFocused, |
| textLayoutState = textLayoutState, |
| textFieldState = transformedState, |
| textFieldSelectionState = textFieldSelectionState, |
| cursorBrush = cursorBrush, |
| writeable = enabled && !readOnly, |
| scrollState = scrollState, |
| orientation = orientation |
| ) |
| ) |
| ) { |
| Box( |
| modifier = TextFieldTextLayoutModifier( |
| textLayoutState = textLayoutState, |
| textFieldState = transformedState, |
| textStyle = textStyle, |
| singleLine = singleLine, |
| onTextLayout = onTextLayout |
| ) |
| ) |
| |
| if (enabled && isFocused && |
| isWindowFocused && textFieldSelectionState.isInTouchMode) { |
| TextFieldSelectionHandles( |
| selectionState = textFieldSelectionState |
| ) |
| if (!readOnly) { |
| TextFieldCursorHandle( |
| selectionState = textFieldSelectionState |
| ) |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| @Composable |
| internal fun TextFieldCursorHandle(selectionState: TextFieldSelectionState) { |
| val cursorHandleState = selectionState.cursorHandle |
| if (cursorHandleState.visible) { |
| CursorHandle( |
| handlePosition = cursorHandleState.position, |
| modifier = Modifier |
| .semantics { |
| this[SelectionHandleInfoKey] = SelectionHandleInfo( |
| handle = Handle.Cursor, |
| position = cursorHandleState.position, |
| anchor = SelectionHandleAnchor.Middle |
| ) |
| } |
| .pointerInput(selectionState) { |
| with(selectionState) { cursorHandleGestures() } |
| }, |
| content = null |
| ) |
| } |
| } |
| |
| @Composable |
| internal fun TextFieldSelectionHandles( |
| selectionState: TextFieldSelectionState |
| ) { |
| val startHandleState = selectionState.startSelectionHandle |
| if (startHandleState.visible) { |
| TextFieldSelectionHandle2( |
| positionProvider = { selectionState.startSelectionHandle.position }, |
| isStartHandle = true, |
| direction = startHandleState.direction, |
| handlesCrossed = startHandleState.handlesCrossed, |
| modifier = Modifier.pointerInput(selectionState) { |
| with(selectionState) { selectionHandleGestures(true) } |
| } |
| ) |
| } |
| |
| val endHandleState = selectionState.endSelectionHandle |
| if (endHandleState.visible) { |
| TextFieldSelectionHandle2( |
| positionProvider = { selectionState.endSelectionHandle.position }, |
| isStartHandle = false, |
| direction = endHandleState.direction, |
| handlesCrossed = endHandleState.handlesCrossed, |
| modifier = Modifier.pointerInput(selectionState) { |
| with(selectionState) { selectionHandleGestures(false) } |
| } |
| ) |
| } |
| } |
| |
| @OptIn(ExperimentalFoundationApi::class) |
| private val DefaultTextFieldDecorator = TextFieldDecorator { it() } |