feat: split String and TextFieldValue in TextField and add pointer state for desktop

This commit is contained in:
2023-11-19 05:32:21 +08:00
parent 583af4601e
commit d58f7d283f
4 changed files with 267 additions and 12 deletions

View File

@@ -0,0 +1,28 @@
/*
* Flexi UI - A flexible and useful UI component library.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiUI
*
* Apache License Version 2.0
*
* 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
*
* https://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.
*
* This file is created by fankes on 2023/11/19.
*/
@file:Suppress("unused")
package com.highcapable.flexiui.component
import androidx.compose.ui.Modifier
internal actual fun Modifier.pointerHoverState(state: TextFieldPoinerState) = this

View File

@@ -49,6 +49,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -63,8 +64,10 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -107,8 +110,8 @@ data class TextFieldStyle(
@Composable @Composable
fun TextField( fun TextField(
value: String, value: TextFieldValue,
onValueChange: (String) -> Unit, onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
colors: TextFieldColors = TextField.colors, colors: TextFieldColors = TextField.colors,
style: TextFieldStyle = TextField.style, style: TextFieldStyle = TextField.style,
@@ -154,7 +157,7 @@ fun TextField(
enabled = enabled, enabled = enabled,
interactionSource = interactionSource, interactionSource = interactionSource,
modifier = modifier modifier = modifier
) ).pointerHoverState(TextFieldPoinerState.TEXT)
) { ) {
// Note: If minWidth is not 0, a constant width is currently set. // Note: If minWidth is not 0, a constant width is currently set.
// At this time, the child layout must be completely filled into the parent layout. // At this time, the child layout must be completely filled into the parent layout.
@@ -189,7 +192,7 @@ fun TextField(
cursorBrush = SolidColor(colors.cursorColor), cursorBrush = SolidColor(colors.cursorColor),
decorationBox = { decorationBox = {
TextFieldDecorationBox( TextFieldDecorationBox(
value = value, value = value.text,
placeholder = placeholder, placeholder = placeholder,
innerTextField = it innerTextField = it
) )
@@ -210,9 +213,60 @@ fun TextField(
} }
@Composable @Composable
fun PasswordTextField( fun TextField(
value: String, value: String,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
colors: TextFieldColors = TextField.colors,
style: TextFieldStyle = TextField.style,
enabled: Boolean = true,
readOnly: Boolean = false,
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,
onTextLayout: (TextLayoutResult) -> Unit = {},
focusRequester: FocusRequester = remember { FocusRequester() },
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
header: @Composable (() -> Unit)? = null,
placeholder: @Composable () -> Unit = {},
footer: @Composable (() -> Unit)? = null,
textStyle: TextStyle = TextField.textStyle
) {
var textFieldValue by remember { mutableStateOf(TextFieldValue(value)) }
TextField(
value = textFieldValue,
onValueChange = {
textFieldValue = it
onValueChange(it.text)
},
modifier = modifier,
colors = colors,
style = style,
enabled = enabled,
readOnly = readOnly,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
singleLine = singleLine,
maxLines = maxLines,
minLines = minLines,
visualTransformation = visualTransformation,
onTextLayout = onTextLayout,
focusRequester = focusRequester,
interactionSource = interactionSource,
header = header,
placeholder = placeholder,
footer = footer,
textStyle = textStyle
)
}
@Composable
fun PasswordTextField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
defaultPasswordVisible: Boolean = false, defaultPasswordVisible: Boolean = false,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
colors: TextFieldColors = TextField.colors, colors: TextFieldColors = TextField.colors,
@@ -231,7 +285,7 @@ fun PasswordTextField(
textStyle: TextStyle = TextField.textStyle textStyle: TextStyle = TextField.textStyle
) { ) {
var passwordVisible by remember { mutableStateOf(defaultPasswordVisible) } var passwordVisible by remember { mutableStateOf(defaultPasswordVisible) }
if (value.isEmpty()) passwordVisible = defaultPasswordVisible if (value.text.isEmpty()) passwordVisible = defaultPasswordVisible
TextField( TextField(
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,
@@ -258,9 +312,9 @@ fun PasswordTextField(
modifier = Modifier.width(DefaultDecorIconSize), modifier = Modifier.width(DefaultDecorIconSize),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
val animatedSize by animateDpAsState(if (value.isNotEmpty()) DefaultDecorIconSize else 0.dp) val animatedSize by animateDpAsState(if (value.text.isNotEmpty()) DefaultDecorIconSize else 0.dp)
IconToggleButton( IconToggleButton(
modifier = Modifier.size(animatedSize), modifier = Modifier.size(animatedSize).pointerHoverState(TextFieldPoinerState.NORMAL),
style = IconButton.style.copy(padding = DefaultDecorIconPadding), style = IconButton.style.copy(padding = DefaultDecorIconPadding),
checked = passwordVisible, checked = passwordVisible,
onCheckedChange = { onCheckedChange = {
@@ -277,9 +331,56 @@ fun PasswordTextField(
} }
@Composable @Composable
fun BackspaceTextField( fun PasswordTextField(
value: String, value: String,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
defaultPasswordVisible: Boolean = false,
modifier: Modifier = Modifier,
colors: TextFieldColors = TextField.colors,
style: TextFieldStyle = TextField.style,
enabled: Boolean = true,
readOnly: Boolean = false,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
normalVisualTransformation: VisualTransformation = VisualTransformation.None,
secretVisualTransformation: VisualTransformation = PasswordVisualTransformation(),
onTextLayout: (TextLayoutResult) -> Unit = {},
focusRequester: FocusRequester = remember { FocusRequester() },
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
header: @Composable (() -> Unit)? = null,
placeholder: @Composable () -> Unit = {},
textStyle: TextStyle = TextField.textStyle
) {
var textFieldValue by remember { mutableStateOf(TextFieldValue(value)) }
PasswordTextField(
value = textFieldValue,
onValueChange = {
textFieldValue = it
onValueChange(it.text)
},
defaultPasswordVisible = defaultPasswordVisible,
modifier = modifier,
colors = colors,
style = style,
enabled = enabled,
readOnly = readOnly,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
normalVisualTransformation = normalVisualTransformation,
secretVisualTransformation = secretVisualTransformation,
onTextLayout = onTextLayout,
focusRequester = focusRequester,
interactionSource = interactionSource,
header = header,
placeholder = placeholder,
textStyle = textStyle
)
}
@Composable
fun BackspaceTextField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
colors: TextFieldColors = TextField.colors, colors: TextFieldColors = TextField.colors,
style: TextFieldStyle = TextField.style, style: TextFieldStyle = TextField.style,
@@ -322,13 +423,18 @@ fun BackspaceTextField(
modifier = Modifier.width(DefaultDecorIconSize), modifier = Modifier.width(DefaultDecorIconSize),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
val animatedSize by animateDpAsState(if (value.isNotEmpty()) DefaultDecorIconSize else 0.dp) val animatedSize by animateDpAsState(if (value.text.isNotEmpty()) DefaultDecorIconSize else 0.dp)
IconButton( IconButton(
onClick = { onClick = {
onValueChange.invoke(value.dropLast(1)) val cursorPosition = value.selection.start
if (cursorPosition > 0) {
val newText = value.text.removeRange(cursorPosition - 1, cursorPosition)
val newValue = TextFieldValue(newText, TextRange(cursorPosition - 1))
onValueChange(newValue)
}
focusRequester.requestFocus() focusRequester.requestFocus()
}, },
modifier = Modifier.width(animatedSize), modifier = Modifier.width(animatedSize).pointerHoverState(TextFieldPoinerState.NORMAL),
style = IconButton.style.copy(padding = DefaultDecorIconPadding), style = IconButton.style.copy(padding = DefaultDecorIconPadding),
enabled = enabled, enabled = enabled,
interactionSource = interactionSource interactionSource = interactionSource
@@ -339,6 +445,55 @@ fun BackspaceTextField(
) )
} }
@Composable
fun BackspaceTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
colors: TextFieldColors = TextField.colors,
style: TextFieldStyle = TextField.style,
enabled: Boolean = true,
readOnly: Boolean = false,
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,
onTextLayout: (TextLayoutResult) -> Unit = {},
focusRequester: FocusRequester = remember { FocusRequester() },
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
header: @Composable (() -> Unit)? = null,
placeholder: @Composable () -> Unit = {},
textStyle: TextStyle = TextField.textStyle
) {
var textFieldValue by remember { mutableStateOf(TextFieldValue(value)) }
BackspaceTextField(
value = textFieldValue,
onValueChange = {
textFieldValue = it
onValueChange(it.text)
},
modifier = modifier,
colors = colors,
style = style,
enabled = enabled,
readOnly = readOnly,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
singleLine = singleLine,
maxLines = maxLines,
minLines = minLines,
visualTransformation = visualTransformation,
onTextLayout = onTextLayout,
focusRequester = focusRequester,
interactionSource = interactionSource,
header = header,
placeholder = placeholder,
textStyle = textStyle
)
}
@Composable @Composable
private fun InnerDecorationBox( private fun InnerDecorationBox(
decorTint: Color, decorTint: Color,
@@ -376,6 +531,11 @@ private fun TextFieldStyle(colors: TextFieldColors, content: @Composable () -> U
} }
} }
internal expect fun Modifier.pointerHoverState(state: TextFieldPoinerState): Modifier
@Stable
internal enum class TextFieldPoinerState { NORMAL, TEXT }
private fun Modifier.textField( private fun Modifier.textField(
colors: TextFieldColors, colors: TextFieldColors,
style: TextFieldStyle, style: TextFieldStyle,

View File

@@ -0,0 +1,39 @@
/*
* Flexi UI - A flexible and useful UI component library.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiUI
*
* Apache License Version 2.0
*
* 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
*
* https://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.
*
* This file is created by fankes on 2023/11/19.
*/
@file:Suppress("unused")
package com.highcapable.flexiui.component
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerIcon
import androidx.compose.ui.input.pointer.pointerHoverIcon
import java.awt.Cursor
internal actual fun Modifier.pointerHoverState(state: TextFieldPoinerState) =
pointerHoverIcon(
PointerIcon(
Cursor.getPredefinedCursor(when (state) {
TextFieldPoinerState.NORMAL -> Cursor.DEFAULT_CURSOR
TextFieldPoinerState.TEXT -> Cursor.TEXT_CURSOR
})
)
)

View File

@@ -0,0 +1,28 @@
/*
* Flexi UI - A flexible and useful UI component library.
* Copyright (C) 2019-2023 HighCapable
* https://github.com/BetterAndroid/FlexiUI
*
* Apache License Version 2.0
*
* 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
*
* https://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.
*
* This file is created by fankes on 2023/11/19.
*/
@file:Suppress("unused")
package com.highcapable.flexiui.component
import androidx.compose.ui.Modifier
internal actual fun Modifier.pointerHoverState(state: TextFieldPoinerState) = this