diff --git a/compose/foundation/foundation/src/skikoMain/kotlin/androidx/compose/foundation/text/input/internal/TextInputSession.skiko.kt b/compose/foundation/foundation/src/skikoMain/kotlin/androidx/compose/foundation/text/input/internal/TextInputSession.skiko.kt index 78354b043269c..74d6633474978 100644 --- a/compose/foundation/foundation/src/skikoMain/kotlin/androidx/compose/foundation/text/input/internal/TextInputSession.skiko.kt +++ b/compose/foundation/foundation/src/skikoMain/kotlin/androidx/compose/foundation/text/input/internal/TextInputSession.skiko.kt @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Android Open Source Project + * Copyright 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,14 +17,17 @@ package androidx.compose.foundation.text.input.internal import androidx.compose.foundation.content.internal.ReceiveContentConfiguration +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.PlatformTextInputMethodRequest import androidx.compose.ui.platform.PlatformTextInputSession import androidx.compose.ui.platform.ViewConfiguration +import androidx.compose.ui.text.input.EditCommand +import androidx.compose.ui.text.input.EditProcessor import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeOptions -import kotlinx.coroutines.awaitCancellation +import androidx.compose.ui.text.input.TextFieldValue import kotlinx.coroutines.flow.MutableSharedFlow -// TODO(https://youtrack.jetbrains.com/issue/COMPOSE-733/Merge-1.6.-Apply-changes-for-the-new-text-input) implement internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSession( state: TransformedTextFieldState, layoutState: TextLayoutState, @@ -34,5 +37,54 @@ internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSe stylusHandwritingTrigger: MutableSharedFlow?, viewConfiguration: ViewConfiguration? ): Nothing { - awaitCancellation() -} \ No newline at end of file + val editProcessor = EditProcessor() + fun onEditCommand(commands: List) { + editProcessor.reset( + value = with(state.visualText) { + TextFieldValue( + text = toString(), + selection = selection, + composition = composition + ) + }, + textInputSession = null + ) + + val newValue = editProcessor.apply(commands) + + state.replaceAll(newValue.text) + state.editUntransformedTextAsUser { + val untransformedSelection = state.mapFromTransformed(newValue.selection) + setSelection(untransformedSelection.start, untransformedSelection.end) + + val composition = newValue.composition + if (composition == null) { + commitComposition() + } else { + val untransformedComposition = state.mapFromTransformed(composition) + setComposition(untransformedComposition.start, untransformedComposition.end) + } + } + } + + startInputMethod( + SkikoPlatformTextInputMethodRequest( + state = TextFieldValue( + state.visualText.toString(), + state.visualText.selection, + state.visualText.composition, + ), + imeOptions = imeOptions, + onEditCommand = ::onEditCommand, + onImeAction = onImeAction + ) + ) +} + +@OptIn(ExperimentalComposeUiApi::class) +private data class SkikoPlatformTextInputMethodRequest( + override val state: TextFieldValue, + override val imeOptions: ImeOptions, + override val onEditCommand: (List) -> Unit, + override val onImeAction: ((ImeAction) -> Unit)? +): PlatformTextInputMethodRequest \ No newline at end of file diff --git a/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skikoMain.kt b/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skikoMain.kt index c407eaf314663..f829b47289608 100644 --- a/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skikoMain.kt +++ b/compose/ui/ui-test/src/skikoMain/kotlin/androidx/compose/ui/test/ComposeUiTest.skikoMain.kt @@ -419,7 +419,7 @@ class SkikoComposeUiTest @InternalTestApi constructor( private inner class TestContext : PlatformContext by PlatformContext.Empty { override val windowInfo: WindowInfo = TestWindowInfo() - override val textInputService: PlatformTextInputService = TestTextInputService() + override val textInputService = TestTextInputService() override val rootForTestListener: PlatformContext.RootForTestListener get() = composeRootRegistry diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt index b5f6bdec775e9..83ca55e0327b7 100644 --- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt +++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/input/TextInputService.kt @@ -34,7 +34,6 @@ import androidx.compose.ui.text.TextLayoutResult * close it with [stopInput]. */ // Open for testing purposes. -@Deprecated("Use PlatformTextInputModifierNode instead.") open class TextInputService(private val platformTextInputService: PlatformTextInputService) { private val _currentInputSession: AtomicReference = AtomicReference(null) @@ -294,7 +293,6 @@ class TextInputSession( /** * Platform specific text input service. */ -@Deprecated("Use PlatformTextInputModifierNode instead.") interface PlatformTextInputService { /** * Start text input session for given client. diff --git a/compose/ui/ui/api/desktop/ui.api b/compose/ui/ui/api/desktop/ui.api index 1b30fd4e3fa58..30f4d99fee8d9 100644 --- a/compose/ui/ui/api/desktop/ui.api +++ b/compose/ui/ui/api/desktop/ui.api @@ -3299,6 +3299,8 @@ public abstract interface class androidx/compose/ui/platform/PlatformContext { public fun requestFocus ()Z public fun setPointerIcon (Landroidx/compose/ui/input/pointer/PointerIcon;)V public fun startDrag-12SF9DM (Landroidx/compose/ui/draganddrop/DragAndDropTransferData;JLkotlin/jvm/functions/Function1;)Z + public fun textInputSession (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public static synthetic fun textInputSession$suspendImpl (Landroidx/compose/ui/platform/PlatformContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class androidx/compose/ui/platform/PlatformContext$Companion { @@ -3360,6 +3362,10 @@ public abstract interface class androidx/compose/ui/platform/PlatformTextInputIn } public abstract interface class androidx/compose/ui/platform/PlatformTextInputMethodRequest { + public abstract fun getImeOptions ()Landroidx/compose/ui/text/input/ImeOptions; + public abstract fun getOnEditCommand ()Lkotlin/jvm/functions/Function1; + public abstract fun getOnImeAction ()Lkotlin/jvm/functions/Function1; + public abstract fun getState ()Landroidx/compose/ui/text/input/TextFieldValue; } public abstract interface class androidx/compose/ui/platform/PlatformTextInputModifierNode : androidx/compose/ui/node/DelegatableNode { diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt index 5711ce5b1c94c..c50e465f54500 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.desktop.kt @@ -43,7 +43,7 @@ internal class DesktopTextInputService(private val component: PlatformComponent) data class CurrentInput( var value: TextFieldValue, val onEditCommand: ((List) -> Unit), - val onImeActionPerformed: ((ImeAction) -> Unit), + val onImeActionPerformed: (ImeAction) -> Unit, val imeAction: ImeAction, var focusedRect: Rect? = null ) @@ -64,7 +64,10 @@ internal class DesktopTextInputService(private val component: PlatformComponent) onImeActionPerformed: (ImeAction) -> Unit ) { val input = CurrentInput( - value, onEditCommand, onImeActionPerformed, imeOptions.imeAction + value = value, + onEditCommand = onEditCommand, + onImeActionPerformed = onImeActionPerformed, + imeAction = imeOptions.imeAction ) currentInput = input @@ -83,14 +86,12 @@ internal class DesktopTextInputService(private val component: PlatformComponent) } override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { - currentInput?.let { input -> - input.value = newValue - } + currentInput?.value = newValue } // TODO(https://github.com/JetBrains/compose-jb/issues/2040): probably the position of input method // popup isn't correct now - @Deprecated("This method should not be called, used BringIntoViewRequester instead.") + @Deprecated("This method should not be called, use BringIntoViewRequester instead.") override fun notifyFocusedRect(rect: Rect) { currentInput?.let { input -> input.focusedRect = rect diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt index 844e975e60d84..f06e745070451 100644 --- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.desktop.kt @@ -20,6 +20,8 @@ import androidx.compose.ui.input.key.KeyEvent as ComposeKeyEvent import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalContext import androidx.compose.ui.ComposeFeatureFlags +import androidx.compose.ui.InternalComposeUiApi +import androidx.compose.ui.SessionMutex import androidx.compose.ui.awt.AwtEventListener import androidx.compose.ui.awt.AwtEventListeners import androidx.compose.ui.awt.OnlyValidPrimaryMouseButtonFilter @@ -48,6 +50,8 @@ import androidx.compose.ui.platform.DesktopTextInputService import androidx.compose.ui.platform.EmptyViewConfiguration import androidx.compose.ui.platform.PlatformComponent import androidx.compose.ui.platform.PlatformContext +import androidx.compose.ui.platform.PlatformTextInputMethodRequest +import androidx.compose.ui.platform.PlatformTextInputSessionScope import androidx.compose.ui.platform.PlatformWindowContext import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.platform.WindowInfo @@ -55,7 +59,6 @@ import androidx.compose.ui.platform.a11y.AccessibilityController import androidx.compose.ui.platform.a11y.ComposeSceneAccessible import androidx.compose.ui.scene.skia.SkiaLayerComponent import androidx.compose.ui.semantics.SemanticsOwner -import androidx.compose.ui.text.input.PlatformTextInputService import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection @@ -90,6 +93,8 @@ import javax.swing.JComponent import javax.swing.SwingUtilities import kotlin.coroutines.CoroutineContext import kotlin.math.roundToInt +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.suspendCancellableCoroutine import org.jetbrains.skia.Canvas import org.jetbrains.skiko.ClipRectangle import org.jetbrains.skiko.ExperimentalSkikoApi @@ -682,7 +687,18 @@ internal class ComposeSceneMediator( override val measureDrawLayerBounds: Boolean = this@ComposeSceneMediator.measureDrawLayerBounds override val viewConfiguration: ViewConfiguration = DesktopViewConfiguration() - override val textInputService: PlatformTextInputService = this@ComposeSceneMediator.textInputService + override val textInputService = this@ComposeSceneMediator.textInputService + + private val textInputSessionMutex = SessionMutex() + + override suspend fun textInputSession( + session: suspend PlatformTextInputSessionScope.() -> Nothing + ): Nothing = textInputSessionMutex.withSessionCancellingPrevious( + sessionInitializer = { + DesktopTextInputSession(coroutineScope = it) + }, + session = session + ) override fun setPointerIcon(pointerIcon: PointerIcon) { contentComponent.cursor = @@ -740,6 +756,35 @@ internal class ComposeSceneMediator( get() = contentComponent.density } + @OptIn(InternalComposeUiApi::class) + private inner class DesktopTextInputSession( + coroutineScope: CoroutineScope, + ) : PlatformTextInputSessionScope, CoroutineScope by coroutineScope { + + private val innerSessionMutex = SessionMutex() + + override suspend fun startInputMethod( + request: PlatformTextInputMethodRequest + ): Nothing = innerSessionMutex.withSessionCancellingPrevious( + // This session has no data, just init/dispose tasks. + sessionInitializer = { null } + ) { + (suspendCancellableCoroutine { continuation -> + textInputService.startInput( + value = request.state, + imeOptions = request.imeOptions, + onEditCommand = request.onEditCommand, + onImeActionPerformed = request.onImeAction ?: {} + ) + + continuation.invokeOnCancellation { + textInputService.stopInput() + } + }) + } + } + + private class InvisibleComponent : Component() { fun requestFocusTemporary(): Boolean { return super.requestFocus(true) diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTypeTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTypeTest.kt index deb50733a4d60..5fe4a8b3867bb 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTypeTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/window/window/WindowTypeTest.kt @@ -16,27 +16,41 @@ package androidx.compose.ui.window.window -import androidx.compose.material.TextField +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.material.Text +import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.awt.ComposeWindow import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color import androidx.compose.ui.sendInputEvent import androidx.compose.ui.sendKeyEvent import androidx.compose.ui.sendKeyTypedEvent import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowTestScope import androidx.compose.ui.window.runApplicationTest import com.google.common.truth.Truth.assertThat import java.awt.event.KeyEvent.KEY_PRESSED import java.awt.event.KeyEvent.KEY_RELEASED -import org.junit.Test +import org.junit.experimental.theories.DataPoint +import org.junit.experimental.theories.Theories +import org.junit.experimental.theories.Theory +import org.junit.runner.RunWith /** * Tests for emulate input to the native window on various systems. @@ -45,154 +59,163 @@ import org.junit.Test * All tests can run on all OSes. * The OS names in test names just represent a unique order of input events on these OSes. */ +@RunWith(Theories::class) class WindowTypeTest { - @Test - fun `q, w, space, backspace 4x (English)`() = runTypeTest { + @Theory + internal fun `q, w, space, backspace 4x (English)`( + textFieldKind: TextFieldKind + ) = runTypeTest(textFieldKind, "English") { // q window.sendKeyEvent(81, 'q', KEY_PRESSED) window.sendKeyTypedEvent('q') window.sendKeyEvent(81, 'q', KEY_RELEASED) - assert(text, "q", selection = TextRange(1), composition = null) + assertStateEquals("q", selection = TextRange(1), composition = null) // w window.sendKeyEvent(87, 'w', KEY_PRESSED) window.sendKeyTypedEvent('w') window.sendKeyEvent(87, 'w', KEY_RELEASED) - assert(text, "qw", selection = TextRange(2), composition = null) + assertStateEquals("qw", selection = TextRange(2), composition = null) // space window.sendKeyEvent(32, ' ', KEY_PRESSED) window.sendKeyTypedEvent(' ') window.sendKeyEvent(32, ' ', KEY_RELEASED) - assert(text, "qw ", selection = TextRange(3), composition = null) + assertStateEquals("qw ", selection = TextRange(3), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "qw", selection = TextRange(2), composition = null) + assertStateEquals("qw", selection = TextRange(2), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "q", selection = TextRange(1), composition = null) + assertStateEquals("q", selection = TextRange(1), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) } - @Test - fun `q, w, space, backspace 4x (Russian)`() = runTypeTest { + @Theory + internal fun `q, w, space, backspace 4x (Russian)`( + textFieldKind: TextFieldKind + ) = runTypeTest(textFieldKind, "Russian") { // q window.sendKeyEvent(81, 'й', KEY_PRESSED) window.sendKeyTypedEvent('й') window.sendKeyEvent(81, 'й', KEY_RELEASED) - assert(text, "й", selection = TextRange(1), composition = null) + assertStateEquals("й", selection = TextRange(1), composition = null) // w window.sendKeyEvent(87, 'ц', KEY_PRESSED) window.sendKeyTypedEvent('ц') window.sendKeyEvent(87, 'ц', KEY_RELEASED) - assert(text, "йц", selection = TextRange(2), composition = null) + assertStateEquals("йц", selection = TextRange(2), composition = null) // space window.sendKeyEvent(32, ' ', KEY_PRESSED) window.sendKeyTypedEvent(' ') window.sendKeyEvent(32, ' ', KEY_RELEASED) - assert(text, "йц ", selection = TextRange(3), composition = null) + assertStateEquals("йц ", selection = TextRange(3), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "йц", selection = TextRange(2), composition = null) + assertStateEquals("йц", selection = TextRange(2), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "й", selection = TextRange(1), composition = null) + assertStateEquals("й", selection = TextRange(1), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) } - @Test - fun `f, g, space, backspace 4x (Arabic)`() = runTypeTest { + @Theory + internal fun `f, g, space, backspace 4x (Arabic)`( + textFieldKind: TextFieldKind + ) = runTypeTest(textFieldKind, "Arabic") { // q window.sendKeyEvent(70, 'ب', KEY_PRESSED) window.sendKeyTypedEvent('ب') window.sendKeyEvent(70, 'ب', KEY_RELEASED) - assert(text, "ب", selection = TextRange(1), composition = null) + assertStateEquals("ب", selection = TextRange(1), composition = null) // w window.sendKeyEvent(71, 'ل', KEY_PRESSED) window.sendKeyTypedEvent('ل') window.sendKeyEvent(71, 'ل', KEY_RELEASED) - assert(text, "بل", selection = TextRange(2), composition = null) + assertStateEquals("بل", selection = TextRange(2), composition = null) // space window.sendKeyEvent(32, ' ', KEY_PRESSED) window.sendKeyTypedEvent(' ') window.sendKeyEvent(32, ' ', KEY_RELEASED) - assert(text, "بل ", selection = TextRange(3), composition = null) + assertStateEquals("بل ", selection = TextRange(3), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "بل", selection = TextRange(2), composition = null) + assertStateEquals("بل", selection = TextRange(2), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "ب", selection = TextRange(1), composition = null) + assertStateEquals("ب", selection = TextRange(1), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) } - @Test - fun `q, w, space, backspace 4x (Korean, Windows)`() = runTypeTest { + @Theory + internal fun `q, w, space, backspace 4x (Korean, Windows)`( + textFieldKind: TextFieldKind + ) = runTypeTest(textFieldKind, "Korean, Windows") { // q window.sendInputEvent("ㅂ", 0) window.sendKeyEvent(81, 'q', KEY_RELEASED) - assert(text, "ㅂ", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("ㅂ", selection = TextRange(1), composition = TextRange(0, 1)) // w window.sendInputEvent("ㅂ", 1) window.sendInputEvent("ㅈ", 0) window.sendKeyEvent(87, 'w', KEY_RELEASED) - assert(text, "ㅂㅈ", selection = TextRange(2), composition = TextRange(1, 2)) + assertStateEquals("ㅂㅈ", selection = TextRange(2), composition = TextRange(1, 2)) // space window.sendInputEvent(null, 0) @@ -200,76 +223,80 @@ class WindowTypeTest { window.sendKeyEvent(32, ' ', KEY_PRESSED) window.sendKeyTypedEvent(' ') window.sendKeyEvent(32, ' ', KEY_RELEASED) - assert(text, "ㅂㅈ ", selection = TextRange(3), composition = null) + assertStateEquals("ㅂㅈ ", selection = TextRange(3), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "ㅂㅈ", selection = TextRange(2), composition = null) + assertStateEquals("ㅂㅈ", selection = TextRange(2), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "ㅂ", selection = TextRange(1), composition = null) + assertStateEquals("ㅂ", selection = TextRange(1), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) } - @Test - fun `q, w, backspace 3x (Korean, Windows)`() = runTypeTest { + @Theory + internal fun `q, w, backspace 3x (Korean, Windows)`( + textFieldKind: TextFieldKind + ) = runTypeTest(textFieldKind, "Korean, Windows") { // q window.sendInputEvent("ㅂ", 0) window.sendKeyEvent(81, 'q', KEY_RELEASED) - assert(text, "ㅂ", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("ㅂ", selection = TextRange(1), composition = TextRange(0, 1)) // w window.sendInputEvent("ㅂ", 1) window.sendInputEvent("ㅈ", 0) window.sendKeyEvent(87, 'w', KEY_RELEASED) - assert(text, "ㅂㅈ", selection = TextRange(2), composition = TextRange(1, 2)) + assertStateEquals("ㅂㅈ", selection = TextRange(2), composition = TextRange(1, 2)) // backspace window.sendInputEvent(null, 0) window.sendInputEvent(null, 0) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "ㅂ", selection = TextRange(1), composition = null) + assertStateEquals("ㅂ", selection = TextRange(1), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) } - @Test - fun `f, g, space, backspace 3x (Korean, Windows)`() = runTypeTest { + @Theory + internal fun `f, g, space, backspace 3x (Korean, Windows)`( + textFieldKind: TextFieldKind + ) = runTypeTest(textFieldKind, "Korean, Windows") { // f window.sendInputEvent("ㄹ", 0) window.sendKeyEvent(81, 'f', KEY_RELEASED) - assert(text, "ㄹ", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("ㄹ", selection = TextRange(1), composition = TextRange(0, 1)) // g window.sendInputEvent("ㅀ", 0) window.sendKeyEvent(87, 'g', KEY_RELEASED) - assert(text, "ㅀ", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("ㅀ", selection = TextRange(1), composition = TextRange(0, 1)) // space window.sendInputEvent(null, 0) @@ -277,115 +304,121 @@ class WindowTypeTest { window.sendKeyEvent(32, ' ', KEY_PRESSED) window.sendKeyTypedEvent(' ') window.sendKeyEvent(32, ' ', KEY_RELEASED) - assert(text, "ㅀ ", selection = TextRange(2), composition = null) + assertStateEquals("ㅀ ", selection = TextRange(2), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "ㅀ", selection = TextRange(1), composition = null) + assertStateEquals("ㅀ", selection = TextRange(1), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) } - @Test - fun `f, g, backspace 2x (Korean, Windows)`() = runTypeTest { + @Theory + internal fun `f, g, backspace 2x (Korean, Windows)`( + textFieldKind: TextFieldKind + ) = runTypeTest(textFieldKind, "Korean, Windows") { // f window.sendInputEvent("ㄹ", 0) window.sendKeyEvent(81, 'f', KEY_RELEASED) - assert(text, "ㄹ", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("ㄹ", selection = TextRange(1), composition = TextRange(0, 1)) // g window.sendInputEvent("ㅀ", 0) window.sendKeyEvent(87, 'g', KEY_RELEASED) - assert(text, "ㅀ", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("ㅀ", selection = TextRange(1), composition = TextRange(0, 1)) // backspace window.sendInputEvent("ㄹ", 0) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "ㄹ", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("ㄹ", selection = TextRange(1), composition = TextRange(0, 1)) // backspace window.sendInputEvent(null, 0) window.sendInputEvent(null, 0) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) } - @Test - fun `q, w, space, backspace 4x (Korean, macOS)`() = runTypeTest { + @Theory + internal fun `q, w, space, backspace 4x (Korean, macOS)`( + textFieldKind: TextFieldKind + ) = runTypeTest(textFieldKind, "Korean, macOS") { // q window.sendInputEvent("ㅂ", 0) window.sendKeyEvent(81, 'ㅂ', KEY_RELEASED) - assert(text, "ㅂ", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("ㅂ", selection = TextRange(1), composition = TextRange(0, 1)) // w window.sendInputEvent("ㅂ", 0) window.sendInputEvent("ㅂ", 1) window.sendInputEvent("ㅈ", 0) window.sendKeyEvent(87, 'ㅈ', KEY_RELEASED) - assert(text, "ㅂㅈ", selection = TextRange(2), composition = TextRange(1, 2)) + assertStateEquals("ㅂㅈ", selection = TextRange(2), composition = TextRange(1, 2)) // space window.sendInputEvent("ㅈ ", 0) window.sendInputEvent("ㅈ ", 2) window.sendKeyEvent(32, ' ', KEY_RELEASED) - assert(text, "ㅂㅈ ", selection = TextRange(3), composition = null) + assertStateEquals("ㅂㅈ ", selection = TextRange(3), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "ㅂㅈ", selection = TextRange(2), composition = null) + assertStateEquals("ㅂㅈ", selection = TextRange(2), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "ㅂ", selection = TextRange(1), composition = null) + assertStateEquals("ㅂ", selection = TextRange(1), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) } - @Test - fun `q, w, backspace 3x (Korean, macOS)`() = runTypeTest { + @Theory + internal fun `q, w, backspace 3x (Korean, macOS)`( + textFieldKind: TextFieldKind + ) = runTypeTest(textFieldKind, "Korean, macOS") { // q window.sendInputEvent("ㅂ", 0) window.sendKeyEvent(81, 'ㅂ', KEY_RELEASED) - assert(text, "ㅂ", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("ㅂ", selection = TextRange(1), composition = TextRange(0, 1)) // w window.sendInputEvent("ㅂ", 0) window.sendInputEvent("ㅂ", 1) window.sendInputEvent("ㅈ", 0) window.sendKeyEvent(87, 'ㅈ', KEY_RELEASED) - assert(text, "ㅂㅈ", selection = TextRange(2), composition = TextRange(1, 2)) + assertStateEquals("ㅂㅈ", selection = TextRange(2), composition = TextRange(1, 2)) // backspace window.sendInputEvent("ㅈ", 0) @@ -393,75 +426,79 @@ class WindowTypeTest { window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "ㅂ", selection = TextRange(1), composition = null) + assertStateEquals("ㅂ", selection = TextRange(1), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) } - // f, g on macOs prints 2 separate symbols (comparing to Windows), so we test t + y - @Test - fun `t, y, space, backspace 3x (Korean, MacOS)`() = runTypeTest { + // f, g on macOS prints 2 separate symbols (comparing to Windows), so we test t + y + @Theory + internal fun `t, y, space, backspace 3x (Korean, macOS)`( + textFieldKind: TextFieldKind + ) = runTypeTest(textFieldKind, "Korean, macOS") { // t window.sendInputEvent("ㅅ", 0) window.sendKeyEvent(84, 'ㅅ', KEY_RELEASED) - assert(text, "ㅅ", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("ㅅ", selection = TextRange(1), composition = TextRange(0, 1)) // y window.sendInputEvent("쇼", 0) window.sendKeyEvent(89, 'ㅛ', KEY_RELEASED) - assert(text, "쇼", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("쇼", selection = TextRange(1), composition = TextRange(0, 1)) // space window.sendInputEvent("쇼 ", 0) window.sendInputEvent("쇼 ", 2) window.sendKeyEvent(32, ' ', KEY_RELEASED) - assert(text, "쇼 ", selection = TextRange(2), composition = null) + assertStateEquals("쇼 ", selection = TextRange(2), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "쇼", selection = TextRange(1), composition = null) + assertStateEquals("쇼", selection = TextRange(1), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) } - @Test - fun `t, y, backspace 2x (Korean, MacOS)`() = runTypeTest { + @Theory + internal fun `t, y, backspace 2x (Korean, macOS)`( + textFieldKind: TextFieldKind + ) = runTypeTest(textFieldKind, "Korean, macOS") { // t window.sendInputEvent("ㅅ", 0) window.sendKeyEvent(84, 'ㅅ', KEY_RELEASED) - assert(text, "ㅅ", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("ㅅ", selection = TextRange(1), composition = TextRange(0, 1)) // y window.sendInputEvent("쇼", 0) window.sendKeyEvent(89, 'ㅛ', KEY_RELEASED) - assert(text, "쇼", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("쇼", selection = TextRange(1), composition = TextRange(0, 1)) // backspace window.sendInputEvent("ㅅ", 0) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "ㅅ", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("ㅅ", selection = TextRange(1), composition = TextRange(0, 1)) // backspace window.sendInputEvent("ㅅ", 0) @@ -469,28 +506,30 @@ class WindowTypeTest { window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) } - @Test - fun `q, w, space, backspace 4x (Korean, Linux)`() = runTypeTest { + @Theory + internal fun `q, w, space, backspace 4x (Korean, Linux)`( + textFieldKind: TextFieldKind + ) = runTypeTest(textFieldKind, "Korean, Linux") { // q window.sendInputEvent("ㅂ", 0) window.sendKeyEvent(0, 'ㅂ', KEY_RELEASED) - assert(text, "ㅂ", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("ㅂ", selection = TextRange(1), composition = TextRange(0, 1)) // w window.sendInputEvent(null, 0) window.sendInputEvent("ㅂ", 1) window.sendInputEvent("ㅈ", 0) window.sendKeyEvent(0, 'ㅈ', KEY_RELEASED) - assert(text, "ㅂㅈ", selection = TextRange(2), composition = TextRange(1, 2)) + assertStateEquals("ㅂㅈ", selection = TextRange(2), composition = TextRange(1, 2)) // space window.sendInputEvent(null, 0) @@ -498,201 +537,308 @@ class WindowTypeTest { window.sendKeyEvent(32, ' ', KEY_PRESSED) window.sendKeyTypedEvent(' ') window.sendKeyEvent(32, ' ', KEY_RELEASED) - assert(text, "ㅂㅈ ", selection = TextRange(3), composition = null) + assertStateEquals("ㅂㅈ ", selection = TextRange(3), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "ㅂㅈ", selection = TextRange(2), composition = null) + assertStateEquals("ㅂㅈ", selection = TextRange(2), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "ㅂ", selection = TextRange(1), composition = null) + assertStateEquals("ㅂ", selection = TextRange(1), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) } - @Test - fun `q, w, space, backspace 3x (Chinese, Windows)`() = runTypeTest { + @Theory + internal fun `q, w, space, backspace 3x (Chinese, Windows)`( + textFieldKind: TextFieldKind + ) = runTypeTest(textFieldKind, "Chinese, Windows") { // q window.sendInputEvent("q", 0) window.sendKeyEvent(81, 'q', KEY_RELEASED) - assert(text, "q", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("q", selection = TextRange(1), composition = TextRange(0, 1)) // w window.sendInputEvent("q'w", 0) window.sendKeyEvent(87, 'w', KEY_RELEASED) - assert(text, "q'w", selection = TextRange(3), composition = TextRange(0, 3)) + assertStateEquals("q'w", selection = TextRange(3), composition = TextRange(0, 3)) // space window.sendInputEvent("請問", 2) window.sendInputEvent(null, 0) window.sendKeyEvent(32, ' ', KEY_RELEASED) - assert(text, "請問", selection = TextRange(2), composition = null) + assertStateEquals("請問", selection = TextRange(2), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "請", selection = TextRange(1), composition = null) + assertStateEquals("請", selection = TextRange(1), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) } - @Test - fun `q, w, backspace 3x (Chinese, Windows)`() = runTypeTest { + @Theory + internal fun `q, w, backspace 3x (Chinese, Windows)`( + textFieldKind: TextFieldKind + ) = runTypeTest(textFieldKind, "Chinese, Windows") { // q window.sendInputEvent("q", 0) window.sendKeyEvent(81, 'q', KEY_RELEASED) - assert(text, "q", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("q", selection = TextRange(1), composition = TextRange(0, 1)) // w window.sendInputEvent("q'w", 0) window.sendKeyEvent(87, 'w', KEY_RELEASED) - assert(text, "q'w", selection = TextRange(3), composition = TextRange(0, 3)) + assertStateEquals("q'w", selection = TextRange(3), composition = TextRange(0, 3)) // backspace window.sendInputEvent("q", 0) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "q", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("q", selection = TextRange(1), composition = TextRange(0, 1)) // backspace window.sendInputEvent(null, 0) window.sendInputEvent(null, 0) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) } - @Test - fun `q, w, space, backspace 3x (Chinese, macOS)`() = runTypeTest { + @Theory + internal fun `q, w, space, backspace 3x (Chinese, macOS)`( + textFieldKind: TextFieldKind + ) = runTypeTest(textFieldKind, "Chinese, macOS") { // q window.sendInputEvent("q", 0) window.sendKeyEvent(81, 'q', KEY_RELEASED) - assert(text, "q", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("q", selection = TextRange(1), composition = TextRange(0, 1)) // w window.sendInputEvent("q w", 0) window.sendKeyEvent(87, 'w', KEY_RELEASED) - assert(text, "q w", selection = TextRange(3), composition = TextRange(0, 3)) + assertStateEquals("q w", selection = TextRange(3), composition = TextRange(0, 3)) // space window.sendInputEvent("请问", 2) window.sendKeyEvent(32, ' ', KEY_RELEASED) - assert(text, "请问", selection = TextRange(2), composition = null) + assertStateEquals("请问", selection = TextRange(2), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "请", selection = TextRange(1), composition = null) + assertStateEquals("请", selection = TextRange(1), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) } - @Test - fun `q, w, backspace 3x (Chinese, macOS)`() = runTypeTest { + @Theory + internal fun `q, w, backspace 3x (Chinese, macOS)`( + textFieldKind: TextFieldKind + ) = runTypeTest(textFieldKind, "Chinese, macOS") { // q window.sendInputEvent("q", 0) window.sendKeyEvent(81, 'q', KEY_RELEASED) - assert(text, "q", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("q", selection = TextRange(1), composition = TextRange(0, 1)) // w window.sendInputEvent("q w", 0) window.sendKeyEvent(87, 'w', KEY_RELEASED) - assert(text, "q w", selection = TextRange(3), composition = TextRange(0, 3)) + assertStateEquals("q w", selection = TextRange(3), composition = TextRange(0, 3)) // backspace window.sendInputEvent("q", 0) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "q", selection = TextRange(1), composition = TextRange(0, 1)) + assertStateEquals("q", selection = TextRange(1), composition = TextRange(0, 1)) // backspace window.sendInputEvent("", 0) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) // backspace window.sendKeyEvent(8, Char(8), KEY_PRESSED) window.sendKeyTypedEvent(Char(8)) window.sendKeyEvent(8, Char(8), KEY_RELEASED) - assert(text, "", selection = TextRange(0), composition = null) + assertStateEquals("", selection = TextRange(0), composition = null) } - private class TypeTestScope(val windowTestScope: WindowTestScope) { - lateinit var window: ComposeWindow - var text by mutableStateOf(TextFieldValue()) + internal interface TypeTestScope { + val windowTestScope: WindowTestScope + val window: ComposeWindow + val text: String + + @Composable + fun TextField() + + suspend fun assertStateEquals(actual: String, selection: TextRange, composition: TextRange?) + } - private fun runTypeTest(body: suspend TypeTestScope.() -> Unit) = runApplicationTest( + private fun runTypeTest( + textFieldKind: TextFieldKind = TextField1, + name: String, + body: suspend TypeTestScope.() -> Unit + ) = runApplicationTest( hasAnimations = true, animationsDelayMillis = 100 ) { - val scope = TypeTestScope(this) + var scope: TypeTestScope? = null launchTestApplication { Window(onCloseRequest = ::exitApplication) { - scope.window = this.window - val focusRequester = FocusRequester() - TextField( - value = scope.text, - onValueChange = { scope.text = it }, - modifier = Modifier.focusRequester(focusRequester) - ) - - LaunchedEffect(focusRequester) { - focusRequester.requestFocus() + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + if (scope == null) { + scope = textFieldKind.createScope(this@runApplicationTest, window) + } + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text("$name ($scope)") + Box(Modifier.border(1.dp, Color.Black).padding(8.dp)) { + scope!!.TextField() + } + } } } } awaitIdle() - scope.body() + scope!!.body() } - private suspend fun TypeTestScope.assert( - actual: TextFieldValue, text: String, selection: TextRange, composition: TextRange? - ) { - windowTestScope.awaitIdle() - assertThat(actual.text).isEqualTo(text) - assertThat(actual.selection).isEqualTo(selection) - assertThat(actual.composition).isEqualTo(composition) + internal fun interface TextFieldKind { + fun createScope(windowTestScope: WindowTestScope, window: ComposeWindow): TypeTestScope + } + + companion object { + @JvmField + @DataPoint + internal val TextField1: TextFieldKind = TextFieldKind { windowTestScope, window -> + object : TypeTestScope { + override val windowTestScope: WindowTestScope + get() = windowTestScope + + override val window: ComposeWindow + get() = window + + private var textFieldValue by mutableStateOf(TextFieldValue()) + + override val text: String + get() = textFieldValue.text + + @Composable + override fun TextField() { + val focusRequester = FocusRequester() + BasicTextField( + value = textFieldValue, + onValueChange = { textFieldValue = it }, + modifier = Modifier.focusRequester(focusRequester) + ) + + LaunchedEffect(focusRequester) { + focusRequester.requestFocus() + } + } + + override suspend fun assertStateEquals( + actual: String, + selection: TextRange, + composition: TextRange? + ) { + windowTestScope.awaitIdle() + assertThat(textFieldValue.text).isEqualTo(actual) + assertThat(textFieldValue.selection).isEqualTo(selection) + assertThat(textFieldValue.composition).isEqualTo(composition) + } + + override fun toString() = "TextField1" + } + } + + @JvmField + @DataPoint + internal val TextField2: TextFieldKind = TextFieldKind { windowTestScope, window -> + object : TypeTestScope { + override val windowTestScope: WindowTestScope + get() = windowTestScope + + override val window: ComposeWindow + get() = window + + private val textFieldState = TextFieldState() + + override val text: String + get() = textFieldState.text.toString() + + @Composable + override fun TextField() { + val focusRequester = FocusRequester() + BasicTextField( + state = textFieldState, + modifier = Modifier.focusRequester(focusRequester) + ) + + LaunchedEffect(focusRequester) { + focusRequester.requestFocus() + } + } + + override suspend fun assertStateEquals( + actual: String, + selection: TextRange, + composition: TextRange? + ) { + windowTestScope.awaitIdle() + assertThat(textFieldState.text.toString()).isEqualTo(actual) + assertThat(textFieldState.selection).isEqualTo(selection) + assertThat(textFieldState.composition).isEqualTo(composition) + + } + + override fun toString() = "TextField2" + } + } } } diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt index 21e7f12f5adaa..e706289146e9b 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt @@ -89,7 +89,6 @@ import androidx.compose.ui.viewinterop.pointerInteropFilter import kotlin.coroutines.CoroutineContext import kotlin.math.max import kotlin.math.min -import kotlinx.coroutines.awaitCancellation /** * Owner of root [LayoutNode]. @@ -320,16 +319,15 @@ internal class RootNodeOwner( override val autofillTree = AutofillTree() override val autofill: Autofill? get() = null override val density get() = this@RootNodeOwner.density - override val textInputService = TextInputService(platformContext.textInputService) + override val textInputService = + TextInputService(platformContext.textInputService) override val softwareKeyboardController = DelegatingSoftwareKeyboardController(textInputService) - // TODO https://youtrack.jetbrains.com/issue/COMPOSE-733/Merge-1.6.-Apply-changes-for-the-new-text-input override suspend fun textInputSession( session: suspend PlatformTextInputSessionScope.() -> Nothing - ): Nothing { - awaitCancellation() - } + ) = platformContext.textInputSession(session) + override val dragAndDropManager = this@RootNodeOwner.dragAndDropManager override val pointerIconService = PointerIconServiceImpl() override val focusOwner get() = this@RootNodeOwner.focusOwner @@ -528,6 +526,7 @@ internal class RootNodeOwner( private inner class PlatformRootForTestImpl : PlatformRootForTest { override val density get() = this@RootNodeOwner.density + @Suppress("OVERRIDE_DEPRECATION") override val textInputService get() = owner.textInputService override val semanticsOwner get() = this@RootNodeOwner.semanticsOwner override val visibleBounds: Rect diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt index 0305a6d288768..64ca6408338a8 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContext.skiko.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Matrix import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.input.InputMode import androidx.compose.ui.input.InputModeManager @@ -38,12 +39,17 @@ import androidx.compose.ui.scene.ComposeScene import androidx.compose.ui.scene.CanvasLayersComposeScene import androidx.compose.ui.semantics.SemanticsNode import androidx.compose.ui.semantics.SemanticsOwner +import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.input.EditCommand import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeOptions +import androidx.compose.ui.text.input.OffsetMapping import androidx.compose.ui.text.input.PlatformTextInputService import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.TextInputService +import androidx.compose.ui.text.input.TextInputSession import kotlin.reflect.KProperty +import kotlinx.coroutines.awaitCancellation /** * Platform context that provides platform-specific bindings. @@ -107,6 +113,13 @@ interface PlatformContext { val viewConfiguration: ViewConfiguration get() = EmptyViewConfiguration val inputModeManager: InputModeManager val textInputService: PlatformTextInputService get() = EmptyPlatformTextInputService + + suspend fun textInputSession( + session: suspend PlatformTextInputSessionScope.() -> Nothing + ): Nothing { + awaitCancellation() + } + val textToolbar: TextToolbar get() = EmptyTextToolbar fun setPointerIcon(pointerIcon: PointerIcon) = Unit @@ -273,3 +286,4 @@ internal class DelegateRootForTestListener : PlatformContext.RootForTestListener } } } + diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformTextInputMethodRequest.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformTextInputMethodRequest.skiko.kt index 704e5c4101b41..6c1fab23db247 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformTextInputMethodRequest.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformTextInputMethodRequest.skiko.kt @@ -16,4 +16,19 @@ package androidx.compose.ui.platform -actual interface PlatformTextInputMethodRequest \ No newline at end of file +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.text.input.EditCommand +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.ImeOptions +import androidx.compose.ui.text.input.TextFieldValue + +actual interface PlatformTextInputMethodRequest { + @ExperimentalComposeUiApi + val state: TextFieldValue + @ExperimentalComposeUiApi + val imeOptions: ImeOptions + @ExperimentalComposeUiApi + val onEditCommand: (List) -> Unit + @ExperimentalComposeUiApi + val onImeAction: ((ImeAction) -> Unit)? +} diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformTextInputSession.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformTextInputSession.skiko.kt index 5c54a5d6e57f7..33cc317a626de 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformTextInputSession.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformTextInputSession.skiko.kt @@ -16,7 +16,6 @@ package androidx.compose.ui.platform -// TODO(https://youtrack.jetbrains.com/issue/COMPOSE-733/Merge-1.6.-Apply-changes-for-the-new-text-input) implement actual interface PlatformTextInputSession { actual suspend fun startInputMethod(request: PlatformTextInputMethodRequest): Nothing } diff --git a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/android/compose/ui/keyboard/KeyboardInsetsTest.kt b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/keyboard/KeyboardInsetsTest.kt similarity index 99% rename from compose/ui/ui/src/uikitInstrumentedTest/kotlin/android/compose/ui/keyboard/KeyboardInsetsTest.kt rename to compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/keyboard/KeyboardInsetsTest.kt index 838b402bccb91..89d4a006c2ad1 100644 --- a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/android/compose/ui/keyboard/KeyboardInsetsTest.kt +++ b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/keyboard/KeyboardInsetsTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package androidx.compose.ui.window +package androidx.compose.ui.keyboard import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column diff --git a/compose/ui/ui/src/uikitInstrumentedTest/kotlin/android/compose/ui/test/UIKitInstrumentedTest.kt b/compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/test/UIKitInstrumentedTest.kt similarity index 100% rename from compose/ui/ui/src/uikitInstrumentedTest/kotlin/android/compose/ui/test/UIKitInstrumentedTest.kt rename to compose/ui/ui/src/uikitInstrumentedTest/kotlin/androidx/compose/ui/test/UIKitInstrumentedTest.kt diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/UIKitTextInputService.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/UIKitTextInputService.uikit.kt index 78924cc85b24a..e654c7decee48 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/UIKitTextInputService.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/platform/UIKitTextInputService.uikit.kt @@ -137,7 +137,7 @@ internal class UIKitTextInputService( currentImeActionHandler = onImeActionPerformed attachIntermediateTextInputView() - textUIView?.input = createSkikoInput(value) + textUIView?.input = createSkikoInput() textUIView?.inputTraits = getUITextInputTraits(imeOptions) showSoftwareKeyboard() @@ -248,9 +248,7 @@ internal class UIKitTextInputService( private fun sendEditCommand(vararg commands: EditCommand) { val commandList = commands.toList() _tempCurrentInputSession?.apply(commandList) - currentInput?.let { input -> - input.onEditCommand(commandList) - } + currentInput?.onEditCommand?.invoke(commandList) } private fun getCursorPos(): Int? { @@ -360,7 +358,7 @@ internal class UIKitTextInputService( textUIView = null } - private fun createSkikoInput(value: TextFieldValue) = object : IOSSkikoInput { + private fun createSkikoInput() = object : IOSSkikoInput { private var floatingCursorTranslation : Offset? = null diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebImeInputService.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebImeInputService.kt index fc333c57467ed..6b171102b3ee1 100644 --- a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebImeInputService.kt +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebImeInputService.kt @@ -39,9 +39,9 @@ internal class WebImeInputService(parentInputService: InputAwareInputService) : ) { backingTextArea = BackingTextArea( - imeOptions, - onEditCommand, - onImeActionPerformed, + imeOptions = imeOptions, + onEditCommand = onEditCommand, + onImeActionPerformed = onImeActionPerformed, processKeyboardEvent = this@WebImeInputService::processKeyboardEvent ) backingTextArea?.register() diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebTextInputService.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebTextInputService.kt index 7c64c60f4e94f..6bfa043470f7f 100644 --- a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebTextInputService.kt +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebTextInputService.kt @@ -18,7 +18,6 @@ package androidx.compose.ui.platform import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.text.input.EditCommand import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeOptions