JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
16.04k stars 1.16k forks source link

[Desktop, Linux] TextField caret position moves in 1.3.0-beta03 #2495

Closed Burtan closed 2 weeks ago

Burtan commented 1 year ago

Hey, I just tried 1.3.0-beta03 from 1.2.1 and realized that writing in a Textfield causes issues on the caret position. After typing e.g. two chars it jumps to the position between the chars instead of staying after the last char.

dima-avdeev-jb commented 1 year ago

What operation system do you have?

dima-avdeev-jb commented 1 year ago

Can you please check caret position in this sample: https://github.com/dima-avdeev-jb/compose-check-focus-with-multiline-text It also uses version 1.3.0-beta03

Burtan commented 1 year ago

I'm on linux gnome. The sample seems to work, I'll double check my application...

Burtan commented 1 year ago

It seems to be related to having the text value being flow based. When I use val text = remember { mutableStateOf("") } it works in my app too. The newest alpha 1.3.0-alpha01-dev862 does not have the problems is also affected.

Burtan commented 1 year ago

Please try this:

fun main() = run {
    val textFlow = MutableStateFlow("")

    application {
        Window(
            state = WindowState(size = DpSize(350.dp, 500.dp)),
            onCloseRequest = ::exitApplication
        ) {
            Column(
                modifier = Modifier.padding(50.dp)
            ) {
                for (x in 1..5) {
                    val textState = textFlow.collectAsState()
                    val text = remember { textState }
                    TextField(
                        value = text.value,
                        onValueChange = { textFlow.value = it },
                        label = { Text("Test") },
                        singleLine = true,
                        modifier = Modifier.moveFocusOnTab()
                            .fillMaxWidth()
                    )
                    Spacer(Modifier.height(10.dp))
                }
            }
        }
    }
}
dima-avdeev-jb commented 1 year ago

Update sample: https://github.com/dima-avdeev-jb/compose-check-focus-with-multiline-text/blob/b9b9041721e71a70fe3f3e2434966e99d30fc5d0/src/main/kotlin/main.kt#L25

It work's good on my MacOS

Burtan commented 1 year ago

I was wrong it is also buggy on 1.3.0-alpha01-dev862

Burtan commented 1 year ago

Windows and Mac work fine. Here is a small clip of the linux behaviour. Bildschirmaufzeichnung vom 2022-11-23, 22-09-48.webm

dima-avdeev-jb commented 1 year ago

Also, reproducible on Manjaro Linux

Burtan commented 1 year ago

When using a qr code scanner with fast text writing, TextField is also occationally missing some chars on Windows

Burtan commented 1 year ago

Any update on this? Will this be fixed? Is it working as intended? The workaround seems to be to only use local states (those created inside the compose function) for textfields.

m-sasha commented 1 year ago

Unfortunately this is a known issue in the implementation of TextField on Google's side. It's described here: https://medium.com/androiddevelopers/effective-state-management-for-textfield-in-compose-d6e5b070fbe5

We'll have to wait for a fix from Google.

mariuszmarzec commented 1 year ago

Basing on article, looks like best option for now is creating stateful text field. For me works 🤷 .

@Composable
fun TextFieldStateful(
    value: String,
    onValueChange: (String) -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    readOnly: Boolean = false,
    textStyle: TextStyle = LocalTextStyle.current,
    label: @Composable (() -> Unit)? = null,
    placeholder: @Composable (() -> Unit)? = null,
    leadingIcon: @Composable (() -> Unit)? = null,
    trailingIcon: @Composable (() -> Unit)? = null,
    isError: Boolean = false,
    visualTransformation: VisualTransformation = VisualTransformation.None,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    keyboardActions: KeyboardActions = KeyboardActions(),
    singleLine: Boolean = false,
    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
    minLines: Int = 1,
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
    shape: Shape =
        MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
    colors: TextFieldColors = TextFieldDefaults.textFieldColors()
) {

    var state by remember(value) { mutableStateOf(value) }

    snapshotFlow { state }
        .mapLatest {
            onValueChange(it)
        }
        .stateIn(
            scope = rememberCoroutineScope(),
            started = SharingStarted.Lazily,
            initialValue = state
       ).collectAsState()

    TextField(
        value = state,
        onValueChange = { newValue ->
            state = newValue
        },
        modifier = modifier,
        enabled = enabled,
        readOnly = readOnly,
        textStyle = textStyle,
        label = label,
        placeholder = placeholder,
        leadingIcon = leadingIcon,
        trailingIcon = trailingIcon,
        isError = isError,
        visualTransformation = visualTransformation,
        keyboardOptions = keyboardOptions,
        keyboardActions = keyboardActions,
        singleLine = singleLine,
        maxLines = maxLines,
        minLines = minLines,
        interactionSource = interactionSource,
        shape = shape,
        colors = colors
    )
}
Nek-12 commented 6 months ago

@mariuszmarzec

You are recreating multiple flows, resubscribing to them and updating state on every recomposition...

okushnikov commented 1 month ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.