Skip to content

Commit b632098

Browse files
authored
Batch commands together when editing text in LegacyPlatformTextInputServiceAdapter (#2722)
1 parent beb188c commit b632098

File tree

7 files changed

+102
-61
lines changed

7 files changed

+102
-61
lines changed

compose/foundation/foundation/src/skikoMain/kotlin/androidx/compose/foundation/text/input/internal/LegacyPlatformTextInputServiceAdapter.skiko.kt

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -114,44 +114,11 @@ internal actual fun createLegacyPlatformTextInputServiceAdapter():
114114
}
115115

116116
val editBlock: (block: TextEditingScope.() -> Unit) -> Unit = { block ->
117-
object : TextEditingScope {
118-
fun runOnEditCommand(command: EditCommand) {
119-
onEditCommand(listOf(command))
120-
}
121-
122-
override fun deleteSurroundingTextInCodePoints(
123-
lengthBeforeCursor: Int,
124-
lengthAfterCursor: Int
125-
) {
126-
runOnEditCommand(
127-
DeleteSurroundingTextCommand(lengthBeforeCursor, lengthAfterCursor)
128-
)
129-
}
130-
131-
override fun commitText(
132-
text: CharSequence,
133-
newCursorPosition: Int
134-
) {
135-
runOnEditCommand(
136-
CommitTextCommand(text.toString(), newCursorPosition)
137-
)
138-
}
139-
140-
override fun setComposingText(
141-
text: CharSequence,
142-
newCursorPosition: Int
143-
) {
144-
runOnEditCommand(
145-
SetComposingTextCommand(text.toString(), newCursorPosition)
146-
)
147-
}
148-
149-
override fun finishComposingText() {
150-
runOnEditCommand(
151-
FinishComposingTextCommand()
152-
)
153-
}
154-
}.block()
117+
val commands = mutableListOf<EditCommand>()
118+
with(TextEditingScope(commands)) {
119+
block()
120+
onEditCommand(commands)
121+
}
155122
}
156123

157124
return SkikoPlatformTextInputMethodRequest(
@@ -169,3 +136,39 @@ internal actual fun createLegacyPlatformTextInputServiceAdapter():
169136
}
170137
}
171138
}
139+
140+
@OptIn(ExperimentalComposeUiApi::class)
141+
private fun TextEditingScope(commands: MutableList<EditCommand>) = object : TextEditingScope {
142+
override fun deleteSurroundingTextInCodePoints(
143+
lengthBeforeCursor: Int,
144+
lengthAfterCursor: Int
145+
) {
146+
commands.add(
147+
DeleteSurroundingTextCommand(lengthBeforeCursor, lengthAfterCursor)
148+
)
149+
}
150+
151+
override fun commitText(
152+
text: CharSequence,
153+
newCursorPosition: Int
154+
) {
155+
commands.add(
156+
CommitTextCommand(text.toString(), newCursorPosition)
157+
)
158+
}
159+
160+
override fun setComposingText(
161+
text: CharSequence,
162+
newCursorPosition: Int
163+
) {
164+
commands.add(
165+
SetComposingTextCommand(text.toString(), newCursorPosition)
166+
)
167+
}
168+
169+
override fun finishComposingText() {
170+
commands.add(
171+
FinishComposingTextCommand()
172+
)
173+
}
174+
}

compose/foundation/foundation/src/skikoMain/kotlin/androidx/compose/foundation/text/input/internal/TextInputSession.skiko.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ private inline fun (() -> TextFieldCharSequence).asTextEditorState() = object :
145145

146146
@OptIn(ExperimentalComposeUiApi::class)
147147
private fun TextEditingScope(buffer: TextFieldBuffer) = object : TextEditingScope {
148-
149148
private var TextFieldBuffer.cursor: Int
150149
get() = if (selection.collapsed) selection.end else -1
151150
set(value) {

compose/foundation/foundation/src/skikoTest/kotlin/androidx/compose/foundation/text/LegacyPlatformTextInputServiceAdapterTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class LegacyPlatformTextInputServiceAdapterTest {
6262
var textClippingRectInRoot: Rect? = null
6363

6464
setContent {
65-
InterceptPlatformTextInput({ request, nextHandler ->
65+
InterceptPlatformTextInput({ request, _ ->
6666
coroutineScope {
6767
launch {
6868
snapshotFlow { request.value() }.collect { value = it }

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopTextInputService2.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package androidx.compose.ui.platform
1818

19+
import androidx.compose.runtime.key
1920
import androidx.compose.runtime.snapshotFlow
2021
import androidx.compose.ui.awt.toAwtRectangleRounded
2122
import androidx.compose.ui.scene.ComposeSceneMediator

compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/BaseWindowTextFieldTest.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import androidx.compose.runtime.Composable
3131
import androidx.compose.runtime.LaunchedEffect
3232
import androidx.compose.runtime.getValue
3333
import androidx.compose.runtime.mutableStateOf
34+
import androidx.compose.runtime.remember
3435
import androidx.compose.runtime.setValue
3536
import androidx.compose.ui.Alignment
3637
import androidx.compose.ui.Modifier
@@ -214,7 +215,7 @@ open class BaseWindowTextFieldTest {
214215
object : TextField1Scope(windowTestScope, window) {
215216
@Composable
216217
override fun TextField() {
217-
val focusRequester = FocusRequester()
218+
val focusRequester = remember { FocusRequester() }
218219
BasicTextField(
219220
value = textFieldValue,
220221
onValueChange = {
@@ -243,7 +244,7 @@ open class BaseWindowTextFieldTest {
243244
object : TextField2Scope(windowTestScope, window) {
244245
@Composable
245246
override fun TextField() {
246-
val focusRequester = FocusRequester()
247+
val focusRequester = remember { FocusRequester() }
247248
BasicTextField(
248249
state = textFieldState,
249250
inputTransformation = inputTransformation,
@@ -270,7 +271,7 @@ open class BaseWindowTextFieldTest {
270271
object : SecureTextFieldScope(windowTestScope, window, TextObfuscationMode.Hidden) {
271272
@Composable
272273
override fun TextField() {
273-
val focusRequester = FocusRequester()
274+
val focusRequester = remember { FocusRequester() }
274275

275276
BasicSecureTextField(
276277
state = textFieldState,

compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTypeTest.kt

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -69,24 +69,24 @@ class WindowTypeTest : BaseWindowTextFieldTest() {
6969
window.sendKeyTypedEvent(Char(8))
7070
window.sendKeyEvent(8, Char(8), KEY_RELEASED)
7171
assertStateEquals("qw", selection = TextRange(2), composition = null)
72-
//
73-
// // backspace
74-
// window.sendKeyEvent(8, Char(8), KEY_PRESSED)
75-
// window.sendKeyTypedEvent(Char(8))
76-
// window.sendKeyEvent(8, Char(8), KEY_RELEASED)
77-
// assertStateEquals("q", selection = TextRange(1), composition = null)
78-
//
79-
// // backspace
80-
// window.sendKeyEvent(8, Char(8), KEY_PRESSED)
81-
// window.sendKeyTypedEvent(Char(8))
82-
// window.sendKeyEvent(8, Char(8), KEY_RELEASED)
83-
// assertStateEquals("", selection = TextRange(0), composition = null)
84-
//
85-
// // backspace
86-
// window.sendKeyEvent(8, Char(8), KEY_PRESSED)
87-
// window.sendKeyTypedEvent(Char(8))
88-
// window.sendKeyEvent(8, Char(8), KEY_RELEASED)
89-
// assertStateEquals("", selection = TextRange(0), composition = null)
72+
73+
// backspace
74+
window.sendKeyEvent(8, Char(8), KEY_PRESSED)
75+
window.sendKeyTypedEvent(Char(8))
76+
window.sendKeyEvent(8, Char(8), KEY_RELEASED)
77+
assertStateEquals("q", selection = TextRange(1), composition = null)
78+
79+
// backspace
80+
window.sendKeyEvent(8, Char(8), KEY_PRESSED)
81+
window.sendKeyTypedEvent(Char(8))
82+
window.sendKeyEvent(8, Char(8), KEY_RELEASED)
83+
assertStateEquals("", selection = TextRange(0), composition = null)
84+
85+
// backspace
86+
window.sendKeyEvent(8, Char(8), KEY_PRESSED)
87+
window.sendKeyTypedEvent(Char(8))
88+
window.sendKeyEvent(8, Char(8), KEY_RELEASED)
89+
assertStateEquals("", selection = TextRange(0), composition = null)
9090
}
9191

9292
@Theory
@@ -771,6 +771,35 @@ class WindowTypeTest : BaseWindowTextFieldTest() {
771771
assertStateEquals("ㅌvㅌ", selection = TextRange(3), composition = null)
772772
}
773773

774+
@Theory
775+
internal fun `q, w, space (Chinese Wubi, macOS)`(
776+
textFieldKind: TextFieldKind<*>
777+
) = runTextFieldTest(textFieldKind, "Chinese Wubi, macOS") {
778+
// Wubi input method sends each composing InputMethodEvent twice for some reason
779+
suspend fun sendInputMethodEventTwice(text: String?, committedCharacterCount: Int = 0) {
780+
repeat(2) {
781+
window.sendInputMethodEvent(text, committedCharacterCount)
782+
awaitIdle() // Wait for recomposition
783+
}
784+
}
785+
786+
// q
787+
sendInputMethodEventTwice("q", 0)
788+
window.sendKeyEvent(81, 'q', KEY_RELEASED)
789+
assertStateEquals("q", selection = TextRange(1), composition = TextRange(0, 1))
790+
791+
// w
792+
sendInputMethodEventTwice("qw", 0)
793+
window.sendKeyEvent(87, 'w', KEY_RELEASED)
794+
assertStateEquals("qw", selection = TextRange(2), composition = TextRange(0, 2))
795+
796+
// space
797+
window.sendInputMethodEvent("", 1)
798+
window.sendKeyEvent(32, ' ', KEY_RELEASED)
799+
assertStateEquals("", selection = TextRange(1), composition = null)
800+
}
801+
802+
774803
// Verifies that each typed character and character replaced by the input service is reported
775804
// (to `inputTransformation`) as a change in the last character only.
776805
// The behavior of `SecureTextField` with `TextObfuscationMode.RevealLastTyped` depends on this,

compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformTextInputMethodRequest.skiko.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,14 @@ actual interface PlatformTextInputMethodRequest {
8484

8585
/**
8686
* Allows the text input service to edit the text.
87+
*
88+
* Note that changes requested in the [TextEditingScope] are buffered and applied, together, to
89+
* the [TextEditorState] only after `block` returns. It is therefore wrong to assume inside
90+
* `block` that the [TextEditorState] has changed following an invocation of a method on
91+
* [TextEditingScope].
92+
*
93+
* If, in the future, the editing `block` requires access to the intermediate state, this state
94+
* should be exposed via [TextEditingScope] itself.
8795
*/
8896
@ExperimentalComposeUiApi
8997
val editText: (block: TextEditingScope.() -> Unit) -> Unit

0 commit comments

Comments
 (0)