mirror of
https://github.com/BetterAndroid/FlexiUI.git
synced 2025-09-08 11:34:18 +08:00
feat: split String and TextFieldValue in TextField and add pointer state for desktop
This commit is contained in:
@@ -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
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
@@ -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
|
Reference in New Issue
Block a user