@@ -6,6 +6,8 @@ import androidx.compose.runtime.MutableState
6
6
import androidx.compose.runtime.mutableStateOf
7
7
import androidx.compose.runtime.remember
8
8
import androidx.compose.runtime.snapshotFlow
9
+ import androidx.compose.ui.text.TextRange
10
+ import androidx.compose.ui.text.input.TextFieldValue
9
11
import com.squareup.workflow1.ui.TextController
10
12
import kotlinx.coroutines.launch
11
13
@@ -42,3 +44,66 @@ import kotlinx.coroutines.launch
42
44
}
43
45
}
44
46
}
47
+
48
+ /* *
49
+ * Exposes the [textValue][TextController.textValue] of a [TextController]
50
+ * as a remembered [MutableState] of [TextFieldValue], suitable for use from `@Composable`
51
+ * functions.
52
+ *
53
+ * Usage:
54
+ *
55
+ * ```
56
+ * var fooText by fooTextController.asMutableTextFieldValueState()
57
+ * BasicTextField(
58
+ * value = fooText,
59
+ * onValueChange = { fooText = it },
60
+ * )
61
+ * ```
62
+ *
63
+ * @param initialSelection The initial range of selection. If [TextRange.start] equals
64
+ * [TextRange.end], then nothing is selected, and the cursor is placed at
65
+ * [TextRange.start]. By default, the cursor will be placed at the end of the text.
66
+ */
67
+ @Composable public fun TextController.asMutableTextFieldValueState (
68
+ initialSelection : TextRange = TextRange (textValue.length),
69
+ ): MutableState <TextFieldValue > {
70
+ val textFieldValue = remember(this ) {
71
+ val actualStart = initialSelection.start.coerceIn(0 , textValue.length)
72
+ val actualEnd = initialSelection.end.coerceIn(actualStart, textValue.length)
73
+ mutableStateOf(
74
+ TextFieldValue (
75
+ text = textValue,
76
+ // We need to set the selection manually when creating new `TextFieldValue` whenever
77
+ // `TextController` changes because the text inside may not be empty.
78
+ selection = TextRange (actualStart, actualEnd),
79
+ )
80
+ )
81
+ }
82
+
83
+ LaunchedEffect (this ) {
84
+ launch {
85
+ // This is to address the case when value of `TextController` is updated within the workflow.
86
+ // By subscribing directly to `onTextChanged` we can use this to also update the textFieldValue.
87
+ onTextChanged
88
+ .collect { newText ->
89
+ // Only update the `textFieldValue` if the new text is different from the current text.
90
+ // This ensures the selection is maintained when the text is updated from the UI side,
91
+ // and is only reset when the text is changed via `TextController`.
92
+ if (textFieldValue.value.text != newText) {
93
+ textFieldValue.value = TextFieldValue (
94
+ text = newText,
95
+ selection = TextRange (newText.length),
96
+ )
97
+ }
98
+ }
99
+ }
100
+
101
+ // Update this `TextController`'s text whenever the `textFieldValue` changes.
102
+ snapshotFlow { textFieldValue.value }
103
+ .collect { newText ->
104
+ textValue = newText.text
105
+ }
106
+ }
107
+
108
+ return textFieldValue
109
+ }
0 commit comments