All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.io.github.lyxnx.compose.pine.InputField.kt Maven / Gradle / Ivy

package io.github.lyxnx.compose.pine

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import io.github.lyxnx.compose.ui.dropShadow
import io.github.lyxnx.compose.ui.ifTrue

/**
 * A Pine Theme input field
 *
 * This supports prefixes and suffixes - separate boxes not part of the actual input field but used to convey extra
 * information, such as a label, along with a leading and trailing icon - elements that are within the bounds of the input
 * field content and is affected by [contentPadding]
 *
 * @param value current value of the input field
 * @param onValueChanged handler that is called when the input value is changed
 * @param modifier modifier to apply to the input field
 * @param placeholder placeholder content to use when [value] is [blank][String.isBlank]
 * @param prefix content to place at the start of the input field, but not within the actual field content itself
 * @param leadingIcon content to place within the bounds of the input field at the start
 * @param trailingIcon content to place within the bounds of the input field at the end
 * @param suffix content to place at the end of the input field, but not within the actual field content itself
 * @param colors colors to apply to the input field in various states
 * @param interactionSource interaction source used to emits events
 * @param contentPadding padding of the main content within the input field
 * @param enabled whether the input field is enabled and responds to input events
 * @param readOnly whether the input field is read only
 * @param placeholderStyle text style to use for [placeholder]
 * @param textStyle text style to use for [value]
 * @param keyboardOptions software keyboard options that contains configuration 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 singleLine when set to true, this text field becomes a single horizontally scrolling text field instead of wrapping onto multiple lines. The keyboard will be informed to not show the return key as the ImeAction. maxLines and minLines are ignored as both are automatically set to 1.
 * @param maxLines the maximum height in terms of maximum number of visible lines. It is required that 1 <= minLines <= maxLines. This parameter is ignored when singleLine is true.
 * @param minLines the minimum height in terms of minimum number of visible lines. It is required that 1 <= minLines <= maxLines. This parameter is ignored when singleLine is true.
 * @param visualTransformation the visual transformation filter for changing the visual representation of the input. By default no visual transformation is applied.
 */
@Composable
@NonRestartableComposable
public fun InputField(
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier,
    placeholder: String,
    prefix: @Composable (() -> Unit)? = null,
    leadingIcon: @Composable (() -> Unit)? = null,
    trailingIcon: @Composable (() -> Unit)? = null,
    suffix: @Composable (() -> Unit)? = null,
    colors: InputFieldColors = InputFieldDefaults.inputFieldColors(),
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    contentPadding: PaddingValues = InputFieldDefaults.ContentPadding,
    enabled: Boolean = true,
    readOnly: Boolean = false,
    placeholderStyle: TextStyle = InputFieldDefaults.PlaceholderStyle,
    textStyle: TextStyle = InputFieldDefaults.TextStyle,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    keyboardActions: KeyboardActions = KeyboardActions.Default,
    singleLine: Boolean = false,
    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
    minLines: Int = 1,
    visualTransformation: VisualTransformation = VisualTransformation.None
) {
    InputField(
        value = value,
        onValueChanged = onValueChanged,
        modifier = modifier,
        placeholder = if (placeholder.isNotBlank()) {
            {
                Text(
                    text = placeholder,
                    style = placeholderStyle
                )
            }
        } else null,
        prefix = prefix,
        leadingIcon = leadingIcon,
        trailingIcon = trailingIcon,
        suffix = suffix,
        colors = colors,
        interactionSource = interactionSource,
        contentPadding = contentPadding,
        enabled = enabled,
        readOnly = readOnly,
        textStyle = textStyle,
        keyboardOptions = keyboardOptions,
        keyboardActions = keyboardActions,
        singleLine = singleLine,
        maxLines = maxLines,
        minLines = minLines,
        visualTransformation = visualTransformation
    )
}

/**
 * The base Pine Theme input field
 *
 * This supports prefixes and suffixes - separate boxes not part of the actual input field but used to convey extra
 * information, such as a label, along with a leading and trailing icon - elements that are within the bounds of the input
 * field content and is affected by [contentPadding]
 *
 * @param value current value of the input field
 * @param onValueChanged handler that is called when the input value is changed
 * @param modifier modifier to apply to the input field
 * @param placeholder placeholder content to use when [value] is [blank][String.isBlank]
 * @param prefix content to place at the start of the input field, but not within the actual field content itself
 * @param leadingIcon content to place within the bounds of the input field at the start
 * @param trailingIcon content to place within the bounds of the input field at the end
 * @param suffix content to place at the end of the input field, but not within the actual field content itself
 * @param colors colors to apply to the input field in various states
 * @param interactionSource interaction source used to emits events
 * @param contentPadding padding of the main content within the input field
 * @param enabled whether the input field is enabled and responds to input events
 * @param readOnly whether the input field is read only
 * @param textStyle text style to use for [value]
 * @param keyboardOptions software keyboard options that contains configuration 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 singleLine when set to true, this text field becomes a single horizontally scrolling text field instead of wrapping onto multiple lines. The keyboard will be informed to not show the return key as the ImeAction. maxLines and minLines are ignored as both are automatically set to 1.
 * @param maxLines the maximum height in terms of maximum number of visible lines. It is required that 1 <= minLines <= maxLines. This parameter is ignored when singleLine is true.
 * @param minLines the minimum height in terms of minimum number of visible lines. It is required that 1 <= minLines <= maxLines. This parameter is ignored when singleLine is true.
 * @param visualTransformation the visual transformation filter for changing the visual representation of the input. By default no visual transformation is applied.
 */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
public fun InputField(
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier,
    placeholder: @Composable (() -> Unit)? = null,
    prefix: @Composable (() -> Unit)? = null,
    leadingIcon: @Composable (() -> Unit)? = null,
    trailingIcon: @Composable (() -> Unit)? = null,
    suffix: @Composable (() -> Unit)? = null,
    colors: InputFieldColors = InputFieldDefaults.inputFieldColors(),
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    contentPadding: PaddingValues = InputFieldDefaults.ContentPadding,
    enabled: Boolean = true,
    readOnly: Boolean = false,
    textStyle: TextStyle = InputFieldDefaults.TextStyle,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    keyboardActions: KeyboardActions = KeyboardActions.Default,
    singleLine: Boolean = false,
    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
    minLines: Int = 1,
    visualTransformation: VisualTransformation = VisualTransformation.None
) {
    val borderColor by colors.borderColor(interactionSource, enabled)
    val backgroundColor by colors.backgroundColor(interactionSource, enabled)
    val contentColor by colors.contentColor(interactionSource, enabled)
    val placeholderColor by colors.placeholderContentColor(interactionSource, enabled)
    val prefixContentColor by colors.prefixColor(interactionSource, enabled)
    val leadingIconColor by colors.leadingIconColor(interactionSource, enabled)
    val trailingIconColor by colors.trailingIconColor(interactionSource, enabled)
    val suffixContentColor by colors.suffixColor(interactionSource, enabled)

    BaseInputField(
        value = value,
        onValueChanged = onValueChanged,
        modifier = modifier,
        colors = colors,
        interactionSource = interactionSource,
        enabled = enabled,
        readOnly = readOnly,
        textStyle = textStyle,
        keyboardOptions = keyboardOptions,
        keyboardActions = keyboardActions,
        singleLine = singleLine,
        maxLines = maxLines,
        minLines = minLines,
        visualTransformation = visualTransformation
    ) { innerTextField ->
        OutlinedTextFieldDefaults.DecorationBox(
            value = value,
            innerTextField = innerTextField,
            enabled = enabled,
            singleLine = singleLine,
            visualTransformation = visualTransformation,
            interactionSource = interactionSource,
            placeholder = placeholder,
            leadingIcon = leadingIcon,
            trailingIcon = trailingIcon,
            prefix = prefix,
            suffix = suffix,
            contentPadding = contentPadding,
            // set all the colours for each state since we decide them further up
            colors = OutlinedTextFieldDefaults.colors(
                unfocusedBorderColor = borderColor,
                focusedBorderColor = borderColor,
                disabledBorderColor = borderColor,
                unfocusedContainerColor = backgroundColor,
                focusedContainerColor = backgroundColor,
                disabledContainerColor = backgroundColor,
                unfocusedLeadingIconColor = leadingIconColor,
                focusedLeadingIconColor = leadingIconColor,
                disabledLeadingIconColor = leadingIconColor,
                unfocusedTrailingIconColor = trailingIconColor,
                focusedTrailingIconColor = trailingIconColor,
                disabledTrailingIconColor = trailingIconColor,
                cursorColor = contentColor,
                unfocusedTextColor = contentColor,
                focusedTextColor = contentColor,
                disabledTextColor = contentColor,
                unfocusedPlaceholderColor = placeholderColor,
                focusedPlaceholderColor = placeholderColor,
                disabledPlaceholderColor = placeholderColor,
                unfocusedPrefixColor = prefixContentColor,
                focusedPrefixColor = prefixContentColor,
                disabledPrefixColor = prefixContentColor,
                focusedSuffixColor = suffixContentColor,
                unfocusedSuffixColor = suffixContentColor,
                disabledSuffixColor = suffixContentColor
            ),
            container = {
                InputFieldDecorationBox(
                    enabled = enabled,
                    interactionSource = interactionSource,
                    colors = colors
                )
            }
        )
    }
}

/**
 * The base Pine Theme input field
 *
 * This is the most basic of input fields, allowing customisation of the decoration box contents. By default the field
 * border, background and shadow will be added to the container
 *
 * @param value current value of the input field
 * @param onValueChanged handler that is called when the input value is changed
 * @param modifier modifier to apply to the input field
 * @param colors colors to apply to the input field in various states
 * @param interactionSource interaction source used to emits events
 * @param enabled whether the input field is enabled and responds to input events
 * @param readOnly whether the input field is read only
 * @param textStyle text style to use for [value]
 * @param keyboardOptions software keyboard options that contains configuration 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 singleLine when set to true, this text field becomes a single horizontally scrolling text field instead of wrapping onto multiple lines. The keyboard will be informed to not show the return key as the ImeAction. maxLines and minLines are ignored as both are automatically set to 1.
 * @param maxLines the maximum height in terms of maximum number of visible lines. It is required that 1 <= minLines <= maxLines. This parameter is ignored when singleLine is true.
 * @param minLines the minimum height in terms of minimum number of visible lines. It is required that 1 <= minLines <= maxLines. This parameter is ignored when singleLine is true.
 * @param visualTransformation the visual transformation filter for changing the visual representation of the input. By default no visual transformation is applied.
 * @param container the internal text field container. This will receive the inner text field
 */
@Composable
@NonRestartableComposable
public fun InputField(
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier,
    colors: InputFieldColors = InputFieldDefaults.inputFieldColors(),
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    enabled: Boolean = true,
    readOnly: Boolean = false,
    textStyle: TextStyle = InputFieldDefaults.TextStyle,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    keyboardActions: KeyboardActions = KeyboardActions.Default,
    singleLine: Boolean = false,
    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
    minLines: Int = 1,
    visualTransformation: VisualTransformation = VisualTransformation.None,
    container: @Composable BoxScope.(innerTextField: @Composable () -> Unit) -> Unit
) {
    BaseInputField(
        value = value,
        onValueChanged = onValueChanged,
        modifier = modifier,
        colors = colors,
        interactionSource = interactionSource,
        enabled = enabled,
        readOnly = readOnly,
        textStyle = textStyle,
        keyboardOptions = keyboardOptions,
        keyboardActions = keyboardActions,
        singleLine = singleLine,
        maxLines = maxLines,
        minLines = minLines,
        visualTransformation = visualTransformation,
        decorationBox = { innerTextField ->
            InputFieldDecorationBox(
                enabled = enabled,
                interactionSource = interactionSource,
                colors = colors
            ) {
                container(innerTextField)
            }
        }
    )
}

@Composable
private fun BaseInputField(
    value: String,
    onValueChanged: (String) -> Unit,
    modifier: Modifier = Modifier,
    colors: InputFieldColors = InputFieldDefaults.inputFieldColors(),
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    enabled: Boolean = true,
    readOnly: Boolean = false,
    textStyle: TextStyle = InputFieldDefaults.TextStyle,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    keyboardActions: KeyboardActions = KeyboardActions.Default,
    singleLine: Boolean = false,
    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
    minLines: Int = 1,
    visualTransformation: VisualTransformation = VisualTransformation.None,
    decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit
) {
    val contentColor by colors.contentColor(interactionSource, enabled)

    // If color is not provided via the text style, use content color as a default
    val textColor = textStyle.color.takeOrElse { contentColor }
    val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))

    CompositionLocalProvider(LocalTextSelectionColors provides colors.selectionColors) {
        BasicTextField(
            value = value,
            onValueChange = onValueChanged,
            modifier = modifier
                .defaultMinSize(
                    minWidth = 240.dp,
                    minHeight = 40.dp
                ),
            enabled = enabled,
            readOnly = readOnly,
            textStyle = mergedTextStyle,
            visualTransformation = visualTransformation,
            keyboardOptions = keyboardOptions,
            keyboardActions = keyboardActions,
            singleLine = singleLine,
            maxLines = maxLines,
            minLines = minLines,
            interactionSource = interactionSource,
            decorationBox = decorationBox
        )
    }
}

@Composable
private fun InputFieldDecorationBox(
    enabled: Boolean,
    interactionSource: MutableInteractionSource,
    colors: InputFieldColors,
    modifier: Modifier = Modifier,
    content: @Composable BoxScope.() -> Unit = {}
) {
    val isFocused by interactionSource.collectIsFocusedAsState()

    val borderColor by colors.borderColor(interactionSource, enabled)
    val backgroundColor by colors.backgroundColor(interactionSource, enabled)

    Box(
        modifier
            .ifTrue(isFocused) {
                dropShadow(
                    color = colors.focusedShadowColor,
                    shape = PineTheme.shapes.small,
                    spread = 2.dp
                )
            }
            .clip(PineTheme.shapes.small)
            .background(backgroundColor)
            .border(
                width = 1.dp,
                color = borderColor,
                shape = PineTheme.shapes.small
            )
    ) {
        content()
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy