From 2752471cfc48eb89e87821213888b1582e76aea8 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Thu, 28 Mar 2024 13:22:07 +0200 Subject: [PATCH 01/11] WindowTypeTest now tests the new BasicTextField2, in addition to the old TextField. --- .../ui/window/window/WindowTypeTest.kt | 454 ++++++++++++------ 1 file changed, 300 insertions(+), 154 deletions(-) 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" + } + } } } From b207a58dba2ca3d966dc0fd107608b39007540a4 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Mon, 29 Jul 2024 13:36:00 +0300 Subject: [PATCH 02/11] [WIP] Support input methods in BTF2. --- .../input/internal/TextInputSession.skiko.kt | 74 +++++++++++++++++-- .../platform/DesktopPlatformInput.desktop.kt | 59 +++++++++++++-- .../ui/scene/ComposeSceneMediator.desktop.kt | 46 ++++++++++++ .../compose/ui/node/RootNodeOwner.skiko.kt | 8 +- .../ui/platform/PlatformContext.skiko.kt | 8 ++ .../PlatformTextInputMethodRequest.skiko.kt | 25 ++++++- .../PlatformTextInputSession.skiko.kt | 1 - 7 files changed, 202 insertions(+), 19 deletions(-) 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..faa586db7971e 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,18 @@ package androidx.compose.foundation.text.input.internal import androidx.compose.foundation.content.internal.ReceiveContentConfiguration +import androidx.compose.ui.platform.PlatformTextInputMethodRequest import androidx.compose.ui.platform.PlatformTextInputSession +import androidx.compose.ui.platform.TextFieldStateAdapter import androidx.compose.ui.platform.ViewConfiguration +import androidx.compose.ui.text.TextRange +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 +38,65 @@ 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 = TransformedTextFieldStateAdapter(state), + imeOptions = imeOptions, + onEditCommand = ::onEditCommand, + onImeAction = onImeAction + ) + ) +} + + +private class TransformedTextFieldStateAdapter( + val state: TransformedTextFieldState +) : TextFieldStateAdapter { + + override val text: CharSequence + get() = state.visualText + + override val selection: TextRange + get() = state.visualText.selection + + override val composition: TextRange? + get() = state.visualText.composition + +} + +private data class SkikoPlatformTextInputMethodRequest( + override val state: TextFieldStateAdapter, + 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/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..0f49e15d2530b 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 @@ -34,14 +34,14 @@ import java.awt.im.InputMethodRequests import java.text.AttributedCharacterIterator import java.text.AttributedString import java.text.CharacterIterator -import java.util.Locale +import java.util.* import kotlin.math.max import kotlin.math.min internal class DesktopTextInputService(private val component: PlatformComponent) : PlatformTextInputService { data class CurrentInput( - var value: TextFieldValue, + var value: TextFieldStateAdapter, val onEditCommand: ((List) -> Unit), val onImeActionPerformed: ((ImeAction) -> Unit), val imeAction: ImeAction, @@ -62,9 +62,40 @@ internal class DesktopTextInputService(private val component: PlatformComponent) imeOptions: ImeOptions, onEditCommand: (List) -> Unit, onImeActionPerformed: (ImeAction) -> Unit + ) { + startInputImpl( + value = TextFieldValueStateAdapter(value), + imeOptions = imeOptions, + onEditCommand = onEditCommand, + onImeActionPerformed = onImeActionPerformed, + ) + } + + fun startInput( + value: TextFieldStateAdapter, + imeOptions: ImeOptions, + onEditCommand: (List) -> Unit, + onImeActionPerformed: ((ImeAction) -> Unit)? + ) { + startInputImpl( + value = value, + imeOptions = imeOptions, + onEditCommand = onEditCommand, + onImeActionPerformed = onImeActionPerformed ?: { } + ) + } + + private fun startInputImpl( + value: TextFieldStateAdapter, + imeOptions: ImeOptions, + onEditCommand: (List) -> Unit, + onImeActionPerformed: (ImeAction) -> Unit ) { val input = CurrentInput( - value, onEditCommand, onImeActionPerformed, imeOptions.imeAction + value = value, + onEditCommand = onEditCommand, + onImeActionPerformed = onImeActionPerformed, + imeAction = imeOptions.imeAction ) currentInput = input @@ -83,14 +114,12 @@ internal class DesktopTextInputService(private val component: PlatformComponent) } override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { - currentInput?.let { input -> - input.value = newValue - } + currentInput?.value = TextFieldValueStateAdapter(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 @@ -242,3 +271,19 @@ private fun AttributedCharacterIterator.toStringFrom(index: Int): String { private val isMac = System.getProperty("os.name").lowercase(Locale.ENGLISH).startsWith("mac") + + +private class TextFieldValueStateAdapter( + val value: TextFieldValue +): TextFieldStateAdapter { + + override val text: CharSequence + get() = value.text + + override val selection: TextRange + get() = value.selection + + override val composition: TextRange? + get() = value.composition + +} \ No newline at end of file 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 a7d2f3ddd0183..ffe323e5100f7 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 @@ -46,6 +48,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 @@ -87,6 +91,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 @@ -668,6 +674,17 @@ internal class ComposeSceneMediator( override val viewConfiguration: ViewConfiguration = DesktopViewConfiguration() override val textInputService: PlatformTextInputService = 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 = (pointerIcon as? AwtCursor)?.cursor ?: Cursor(Cursor.DEFAULT_CURSOR) @@ -718,6 +735,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/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/node/RootNodeOwner.skiko.kt index 5085ae494dc6f..4a6fe879bdf0a 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.InteropViewAnchorModifierNode import kotlin.coroutines.CoroutineContext import kotlin.math.max import kotlin.math.min -import kotlinx.coroutines.awaitCancellation /** * Owner of root [LayoutNode]. @@ -322,12 +321,10 @@ internal class RootNodeOwner( 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: DragAndDropManager = this@RootNodeOwner.dragAndDropManager override val pointerIconService = PointerIconServiceImpl() override val focusOwner get() = this@RootNodeOwner.focusOwner @@ -525,6 +522,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 7eb4d0eee5f74..918f26c2db302 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 @@ -46,6 +46,7 @@ import androidx.compose.ui.text.input.ImeOptions import androidx.compose.ui.text.input.PlatformTextInputService import androidx.compose.ui.text.input.TextFieldValue import kotlin.reflect.KProperty +import kotlinx.coroutines.awaitCancellation /** * Platform context that provides platform-specific bindings. @@ -109,6 +110,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 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..0e572bccba98d 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,27 @@ package androidx.compose.ui.platform -actual interface PlatformTextInputMethodRequest \ No newline at end of file +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.EditCommand +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.ImeOptions + +actual interface PlatformTextInputMethodRequest { + val state: TextFieldStateAdapter + val imeOptions: ImeOptions + val onEditCommand: (List) -> Unit + val onImeAction: ((ImeAction) -> Unit)? +} + +/** + * The purpose of this interface is to provide, in the `ui` module, an adapter for + * `TransformedTextFieldStateAdapter`, which is in the `foundation` module. + * + * It exposes all the properties of `TransformedTextFieldStateAdapter` that are necessary for + * implementing input methods. + */ +interface TextFieldStateAdapter { + val text: CharSequence + val selection: TextRange + val composition: TextRange? +} 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 } From 8a7309b7f139a41172035609d2207e723d8f86a8 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Tue, 13 Aug 2024 16:35:51 +0300 Subject: [PATCH 03/11] Replace the deprecated `PlatformTextInputService` with a new `PlatformContextTextInputService` in `PlatformContext.textInputService` --- .../ui/test/ComposeUiTest.skikoMain.kt | 6 +- .../platform/DesktopPlatformInput.desktop.kt | 3 +- .../ui/scene/ComposeSceneMediator.desktop.kt | 4 +- .../ui/platform/DesktopInputComponentTest.kt | 4 +- .../platform/MacosTextInputService.macos.kt | 3 +- .../androidx/compose/ui/ComposeScene.skiko.kt | 5 +- .../compose/ui/node/RootNodeOwner.skiko.kt | 4 +- .../ui/platform/PlatformContext.skiko.kt | 144 +++++++++++++++++- .../platform/UIKitTextInputService.uikit.kt | 3 +- .../compose/ui/platform/WebImeInputService.kt | 3 +- .../ui/platform/WebKeyboardInputService.kt | 2 +- .../ui/platform/WebTextInputService.kt | 6 +- 12 files changed, 161 insertions(+), 26 deletions(-) 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..158efb15f79de 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 @@ -27,6 +27,7 @@ import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.node.RootForTest import androidx.compose.ui.platform.InfiniteAnimationPolicy import androidx.compose.ui.platform.PlatformContext +import androidx.compose.ui.platform.PlatformContextTextInputService import androidx.compose.ui.platform.WindowInfo import androidx.compose.ui.scene.ComposeScene import androidx.compose.ui.scene.ComposeSceneContext @@ -35,7 +36,6 @@ import androidx.compose.ui.semantics.SemanticsNode 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.PlatformTextInputService import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize @@ -391,7 +391,7 @@ class SkikoComposeUiTest @InternalTestApi constructor( get() = size } - private inner class TestTextInputService : PlatformTextInputService { + private inner class TestTextInputService : PlatformContextTextInputService { var session: Session? = null override fun startInput( @@ -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/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 0f49e15d2530b..c5d70c3b617ef 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 @@ -22,7 +22,6 @@ import androidx.compose.ui.text.input.DeleteSurroundingTextInCodePointsCommand 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.PlatformTextInputService import androidx.compose.ui.text.input.SetComposingTextCommand import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.substring @@ -39,7 +38,7 @@ import kotlin.math.max import kotlin.math.min internal class DesktopTextInputService(private val component: PlatformComponent) : - PlatformTextInputService { + PlatformContextTextInputService { data class CurrentInput( var value: TextFieldStateAdapter, val onEditCommand: ((List) -> Unit), 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 ffe323e5100f7..9fefbfbf925cc 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 @@ -48,6 +48,7 @@ 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.PlatformContextTextInputService import androidx.compose.ui.platform.PlatformTextInputMethodRequest import androidx.compose.ui.platform.PlatformTextInputSessionScope import androidx.compose.ui.platform.PlatformWindowContext @@ -57,7 +58,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 @@ -672,7 +672,7 @@ 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() diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt index be403e1ecb0b1..8249a06db6a78 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt @@ -41,7 +41,7 @@ class DesktopInputComponentTest { val processor = EditProcessor() val input = DesktopTextInputService(PlatformComponent.Empty) - val inputService = TextInputService(input) + val inputService = TextInputService(input.asPlatformTextInputService()) val session = inputService.startInput( value = TextFieldValue(), @@ -88,7 +88,7 @@ class DesktopInputComponentTest { } } val input = DesktopTextInputService(component) - val inputService = TextInputService(input) + val inputService = TextInputService(input.asPlatformTextInputService()) val session = inputService.startInput( value = TextFieldValue(), diff --git a/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/platform/MacosTextInputService.macos.kt b/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/platform/MacosTextInputService.macos.kt index db7ad374c71d8..57cc8c9ef4452 100644 --- a/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/platform/MacosTextInputService.macos.kt +++ b/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/platform/MacosTextInputService.macos.kt @@ -19,10 +19,9 @@ package androidx.compose.ui.platform 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.PlatformTextInputService import androidx.compose.ui.text.input.TextFieldValue -internal class MacosTextInputService : PlatformTextInputService { +internal class MacosTextInputService : PlatformContextTextInputService { data class CurrentInput( var value: TextFieldValue, diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt index 0742e946a381d..dff6d0f0b94dd 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.node.RootForTest import androidx.compose.ui.platform.* import androidx.compose.ui.scene.ComposeSceneContext import androidx.compose.ui.scene.ComposeScenePointer -import androidx.compose.ui.text.input.PlatformTextInputService import androidx.compose.ui.unit.* import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.* @@ -98,7 +97,7 @@ class ComposeScene internal constructor( */ @ExperimentalComposeUiApi constructor( - textInputService: PlatformTextInputService, + textInputService: PlatformContextTextInputService, coroutineContext: CoroutineContext = Dispatchers.Unconfined, density: Density = Density(1f), layoutDirection: LayoutDirection = LayoutDirection.Ltr, @@ -149,7 +148,7 @@ class ComposeScene internal constructor( * schedule the next [render] in your rendering loop. */ constructor( - textInputService: PlatformTextInputService, + textInputService: PlatformContextTextInputService, coroutineContext: CoroutineContext = Dispatchers.Unconfined, density: Density = Density(1f), invalidate: () -> Unit = {} 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 4a6fe879bdf0a..4d50175d7f334 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 @@ -66,6 +66,7 @@ import androidx.compose.ui.platform.PlatformRootForTest import androidx.compose.ui.platform.PlatformTextInputSessionScope import androidx.compose.ui.platform.RenderNodeLayer import androidx.compose.ui.platform.asDragAndDropManager +import androidx.compose.ui.platform.asPlatformTextInputService import androidx.compose.ui.scene.ComposeScene import androidx.compose.ui.scene.ComposeSceneInputHandler import androidx.compose.ui.scene.ComposeScenePointer @@ -317,7 +318,8 @@ 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.asPlatformTextInputService()) override val softwareKeyboardController = DelegatingSoftwareKeyboardController(textInputService) 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 918f26c2db302..1088a8df02d99 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 @@ -28,6 +28,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 @@ -40,11 +41,15 @@ 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 @@ -109,7 +114,7 @@ interface PlatformContext { val viewConfiguration: ViewConfiguration get() = EmptyViewConfiguration val inputModeManager: InputModeManager - val textInputService: PlatformTextInputService get() = EmptyPlatformTextInputService + val textInputService: PlatformContextTextInputService get() = EmptyPlatformContextTextInputService suspend fun textInputSession( session: suspend PlatformTextInputSessionScope.() -> Nothing @@ -212,7 +217,7 @@ internal object EmptyViewConfiguration : ViewConfiguration { override val touchSlop: Float = 18f } -private object EmptyPlatformTextInputService : PlatformTextInputService { +private object EmptyPlatformContextTextInputService : PlatformContextTextInputService { override fun startInput( value: TextFieldValue, imeOptions: ImeOptions, @@ -293,3 +298,138 @@ internal class DelegateRootForTestListener : PlatformContext.RootForTestListener } } } + +/** + * Platform specific text input service. + * + * This is a non-deprecated version of [PlatformTextInputService], which is needed because we can't + * expose deprecated APIs in [PlatformContext]. + */ +interface PlatformContextTextInputService { + /** + * Start text input session for given client. + * + * @see TextInputService.startInput + */ + fun startInput( + value: TextFieldValue, + imeOptions: ImeOptions, + onEditCommand: (List) -> Unit, + onImeActionPerformed: (ImeAction) -> Unit + ) + + /** + * Restart input and show the keyboard. This should only be called when starting a new + * `PlatformTextInputModifierNode.textInputSession`. + * + * @see TextInputService.startInput + */ + fun startInput() {} + + /** + * Stop text input session. + * + * @see TextInputService.stopInput + */ + fun stopInput() + + /** + * Request showing onscreen keyboard + * + * There is no guarantee nor callback of the result of this API. + * + * @see TextInputService.showSoftwareKeyboard + */ + fun showSoftwareKeyboard() + + /** + * Hide software keyboard + * + * @see TextInputService.hideSoftwareKeyboard + */ + fun hideSoftwareKeyboard() + + /** + * Notify the new editor model to IME. + * + * @see TextInputSession.updateState + */ + fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) + + /** + * Notify the focused rectangle to the system. + * + * The system can ignore this information or use it to for additional functionality. + * + * For example, desktop systems show a popup near the focused input area (for some languages). + */ + // TODO(b/262648050) Try to find a better API. + fun notifyFocusedRect(rect: Rect) { + } + + /** + * Notify the input service of layout and position changes. + * + * @see TextInputSession.updateTextLayoutResult + */ + fun updateTextLayoutResult( + textFieldValue: TextFieldValue, + offsetMapping: OffsetMapping, + textLayoutResult: TextLayoutResult, + textFieldToRootTransform: (Matrix) -> Unit, + innerTextFieldBounds: Rect, + decorationBoxBounds: Rect + ) { + } +} + +@Suppress("DEPRECATION") +fun PlatformContextTextInputService.asPlatformTextInputService() = object : PlatformTextInputService { + override fun startInput( + value: TextFieldValue, + imeOptions: ImeOptions, + onEditCommand: (List) -> Unit, + onImeActionPerformed: (ImeAction) -> Unit + ) { + this@asPlatformTextInputService.startInput( + value = value, + imeOptions = imeOptions, + onEditCommand = onEditCommand, + onImeActionPerformed = onImeActionPerformed + ) + } + + override fun startInput() = this@asPlatformTextInputService.startInput() + + override fun stopInput() = this@asPlatformTextInputService.stopInput() + + override fun showSoftwareKeyboard() = this@asPlatformTextInputService.showSoftwareKeyboard() + + override fun hideSoftwareKeyboard() = this@asPlatformTextInputService.hideSoftwareKeyboard() + + override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { + this@asPlatformTextInputService.updateState(oldValue, newValue) + } + + override fun notifyFocusedRect(rect: Rect) { + this@asPlatformTextInputService.notifyFocusedRect(rect) + } + + override fun updateTextLayoutResult( + textFieldValue: TextFieldValue, + offsetMapping: OffsetMapping, + textLayoutResult: TextLayoutResult, + textFieldToRootTransform: (Matrix) -> Unit, + innerTextFieldBounds: Rect, + decorationBoxBounds: Rect + ) { + this@asPlatformTextInputService.updateTextLayoutResult( + textFieldValue = textFieldValue, + offsetMapping = offsetMapping, + textLayoutResult = textLayoutResult, + textFieldToRootTransform = textFieldToRootTransform, + innerTextFieldBounds = innerTextFieldBounds, + decorationBoxBounds = decorationBoxBounds + ) + } +} 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 912925e72940f..4dbfa8531bfe7 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 @@ -33,7 +33,6 @@ import androidx.compose.ui.text.input.FinishComposingTextCommand 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.SetComposingRegionCommand import androidx.compose.ui.text.input.SetComposingTextCommand import androidx.compose.ui.text.input.SetSelectionCommand @@ -66,7 +65,7 @@ internal class UIKitTextInputService( * Erasure happens due to K/N not supporting Obj-C lightweight generics. */ private val onKeyboardPresses: (Set<*>) -> Unit, -) : PlatformTextInputService, TextToolbar { +) : PlatformContextTextInputService, TextToolbar { private val rootView get() = rootViewProvider() private var currentInput: CurrentInput? = 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..9a65b92c111a4 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 @@ -20,10 +20,9 @@ import androidx.compose.ui.geometry.Rect 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.PlatformTextInputService import androidx.compose.ui.text.input.TextFieldValue -internal class WebImeInputService(parentInputService: InputAwareInputService) : PlatformTextInputService, InputAwareInputService by parentInputService { +internal class WebImeInputService(parentInputService: InputAwareInputService) : PlatformContextTextInputService, InputAwareInputService by parentInputService { private var backingTextArea: BackingTextArea? = null set(value) { diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebKeyboardInputService.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebKeyboardInputService.kt index 1b99719669887..4c8dd97001095 100644 --- a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebKeyboardInputService.kt +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebKeyboardInputService.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.text.input.ImeOptions import androidx.compose.ui.text.input.PlatformTextInputService import androidx.compose.ui.text.input.TextFieldValue -internal class WebKeyboardInputService : PlatformTextInputService { +internal class WebKeyboardInputService : PlatformContextTextInputService { override fun startInput( value: TextFieldValue, imeOptions: ImeOptions, 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..a2eee2fd95f57 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,11 +18,9 @@ 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 -import androidx.compose.ui.text.input.PlatformTextInputService import androidx.compose.ui.text.input.TextFieldValue import org.w3c.dom.events.KeyboardEvent @@ -32,11 +30,11 @@ internal interface InputAwareInputService { fun isVirtualKeyboard(): Boolean } -internal abstract class WebTextInputService : PlatformTextInputService, InputAwareInputService { +internal abstract class WebTextInputService : PlatformContextTextInputService, InputAwareInputService { private val webImeInputService = WebImeInputService(this) private val webKeyboardInputService = WebKeyboardInputService() - private fun delegatedService(): PlatformTextInputService { + private fun delegatedService(): PlatformContextTextInputService { return if (isVirtualKeyboard()) webImeInputService else webKeyboardInputService } From a1f84927dad003bff15e6629ca3c93338d2c0646 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Tue, 13 Aug 2024 17:58:34 +0300 Subject: [PATCH 04/11] Simplify `PlatformContextTextInputService` by using `TextFieldStateAdapter` in it. --- .../ui/test/ComposeUiTest.skikoMain.kt | 10 ++-- .../platform/DesktopPlatformInput.desktop.kt | 51 ++----------------- .../ui/scene/ComposeSceneMediator.desktop.kt | 1 - .../platform/MacosTextInputService.macos.kt | 9 ++-- .../ui/platform/PlatformContext.skiko.kt | 23 +++++---- .../PlatformTextInputMethodRequest.skiko.kt | 38 ++++++++++++++ .../platform/UIKitTextInputService.uikit.kt | 23 ++++----- .../compose/ui/platform/BackingTextArea.kt | 11 ++-- .../compose/ui/platform/WebImeInputService.kt | 13 +++-- .../ui/platform/WebKeyboardInputService.kt | 8 ++- .../ui/platform/WebTextInputService.kt | 7 ++- 11 files changed, 91 insertions(+), 103 deletions(-) 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 158efb15f79de..e406402ddaada 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 @@ -28,6 +28,7 @@ import androidx.compose.ui.node.RootForTest import androidx.compose.ui.platform.InfiniteAnimationPolicy import androidx.compose.ui.platform.PlatformContext import androidx.compose.ui.platform.PlatformContextTextInputService +import androidx.compose.ui.platform.TextFieldStateAdapter import androidx.compose.ui.platform.WindowInfo import androidx.compose.ui.scene.ComposeScene import androidx.compose.ui.scene.ComposeSceneContext @@ -36,7 +37,6 @@ import androidx.compose.ui.semantics.SemanticsNode 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 import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import kotlin.coroutines.CoroutineContext @@ -395,15 +395,15 @@ class SkikoComposeUiTest @InternalTestApi constructor( var session: Session? = null override fun startInput( - value: TextFieldValue, + value: TextFieldStateAdapter, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, - onImeActionPerformed: (ImeAction) -> Unit + onImeActionPerformed: ((ImeAction) -> Unit)? ) { session = Session( imeOptions = imeOptions, onEditCommand = onEditCommand, - onImeActionPerformed = onImeActionPerformed + onImeActionPerformed = onImeActionPerformed ?: { } ) } @@ -413,7 +413,7 @@ class SkikoComposeUiTest @InternalTestApi constructor( override fun showSoftwareKeyboard() = Unit override fun hideSoftwareKeyboard() = Unit - override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) = Unit + override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) = Unit } private inner class TestContext : PlatformContext by PlatformContext.Empty { 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 c5d70c3b617ef..46af900ca4660 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 @@ -23,7 +23,6 @@ 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.SetComposingTextCommand -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.substring import java.awt.Rectangle import java.awt.event.InputMethodEvent @@ -42,7 +41,7 @@ internal class DesktopTextInputService(private val component: PlatformComponent) data class CurrentInput( var value: TextFieldStateAdapter, val onEditCommand: ((List) -> Unit), - val onImeActionPerformed: ((ImeAction) -> Unit), + val onImeActionPerformed: ((ImeAction) -> Unit)?, val imeAction: ImeAction, var focusedRect: Rect? = null ) @@ -57,38 +56,10 @@ internal class DesktopTextInputService(private val component: PlatformComponent) var needToDeletePreviousChar: Boolean = false override fun startInput( - value: TextFieldValue, - imeOptions: ImeOptions, - onEditCommand: (List) -> Unit, - onImeActionPerformed: (ImeAction) -> Unit - ) { - startInputImpl( - value = TextFieldValueStateAdapter(value), - imeOptions = imeOptions, - onEditCommand = onEditCommand, - onImeActionPerformed = onImeActionPerformed, - ) - } - - fun startInput( value: TextFieldStateAdapter, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, onImeActionPerformed: ((ImeAction) -> Unit)? - ) { - startInputImpl( - value = value, - imeOptions = imeOptions, - onEditCommand = onEditCommand, - onImeActionPerformed = onImeActionPerformed ?: { } - ) - } - - private fun startInputImpl( - value: TextFieldStateAdapter, - imeOptions: ImeOptions, - onEditCommand: (List) -> Unit, - onImeActionPerformed: (ImeAction) -> Unit ) { val input = CurrentInput( value = value, @@ -112,8 +83,8 @@ internal class DesktopTextInputService(private val component: PlatformComponent) override fun hideSoftwareKeyboard() { } - override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { - currentInput?.value = TextFieldValueStateAdapter(newValue) + override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) { + currentInput?.value = newValue } // TODO(https://github.com/JetBrains/compose-jb/issues/2040): probably the position of input method @@ -270,19 +241,3 @@ private fun AttributedCharacterIterator.toStringFrom(index: Int): String { private val isMac = System.getProperty("os.name").lowercase(Locale.ENGLISH).startsWith("mac") - - -private class TextFieldValueStateAdapter( - val value: TextFieldValue -): TextFieldStateAdapter { - - override val text: CharSequence - get() = value.text - - override val selection: TextRange - get() = value.selection - - override val composition: TextRange? - get() = value.composition - -} \ No newline at end of file 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 9fefbfbf925cc..6332b6eee2623 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 @@ -48,7 +48,6 @@ 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.PlatformContextTextInputService import androidx.compose.ui.platform.PlatformTextInputMethodRequest import androidx.compose.ui.platform.PlatformTextInputSessionScope import androidx.compose.ui.platform.PlatformWindowContext diff --git a/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/platform/MacosTextInputService.macos.kt b/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/platform/MacosTextInputService.macos.kt index 57cc8c9ef4452..76019b8c42ccc 100644 --- a/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/platform/MacosTextInputService.macos.kt +++ b/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/platform/MacosTextInputService.macos.kt @@ -19,22 +19,21 @@ package androidx.compose.ui.platform 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 internal class MacosTextInputService : PlatformContextTextInputService { data class CurrentInput( - var value: TextFieldValue, + var value: TextFieldStateAdapter, val onEditCommand: ((List) -> Unit), ) private var currentInput: CurrentInput? = null override fun startInput( - value: TextFieldValue, + value: TextFieldStateAdapter, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, - onImeActionPerformed: (ImeAction) -> Unit + onImeActionPerformed: ((ImeAction) -> Unit)? ) { currentInput = CurrentInput( value, @@ -54,7 +53,7 @@ internal class MacosTextInputService : PlatformContextTextInputService { //do nothing } - override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { + override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) { currentInput?.let { input -> input.value = newValue } 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 1088a8df02d99..f52efef9ec49b 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 @@ -219,16 +219,16 @@ internal object EmptyViewConfiguration : ViewConfiguration { private object EmptyPlatformContextTextInputService : PlatformContextTextInputService { override fun startInput( - value: TextFieldValue, + value: TextFieldStateAdapter, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, - onImeActionPerformed: (ImeAction) -> Unit + onImeActionPerformed: ((ImeAction) -> Unit)? ) = Unit override fun stopInput() = Unit override fun showSoftwareKeyboard() = Unit override fun hideSoftwareKeyboard() = Unit - override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) = Unit + override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) = Unit } private object EmptyTextToolbar : TextToolbar { @@ -312,10 +312,10 @@ interface PlatformContextTextInputService { * @see TextInputService.startInput */ fun startInput( - value: TextFieldValue, + value: TextFieldStateAdapter, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, - onImeActionPerformed: (ImeAction) -> Unit + onImeActionPerformed: ((ImeAction) -> Unit)? ) /** @@ -354,7 +354,7 @@ interface PlatformContextTextInputService { * * @see TextInputSession.updateState */ - fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) + fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) /** * Notify the focused rectangle to the system. @@ -373,7 +373,7 @@ interface PlatformContextTextInputService { * @see TextInputSession.updateTextLayoutResult */ fun updateTextLayoutResult( - textFieldValue: TextFieldValue, + textFieldValue: TextFieldStateAdapter, offsetMapping: OffsetMapping, textLayoutResult: TextLayoutResult, textFieldToRootTransform: (Matrix) -> Unit, @@ -392,7 +392,7 @@ fun PlatformContextTextInputService.asPlatformTextInputService() = object : Plat onImeActionPerformed: (ImeAction) -> Unit ) { this@asPlatformTextInputService.startInput( - value = value, + value = value.asTextFieldStateAdapter(), imeOptions = imeOptions, onEditCommand = onEditCommand, onImeActionPerformed = onImeActionPerformed @@ -408,7 +408,10 @@ fun PlatformContextTextInputService.asPlatformTextInputService() = object : Plat override fun hideSoftwareKeyboard() = this@asPlatformTextInputService.hideSoftwareKeyboard() override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { - this@asPlatformTextInputService.updateState(oldValue, newValue) + this@asPlatformTextInputService.updateState( + oldValue = oldValue?.asTextFieldStateAdapter(), + newValue = newValue.asTextFieldStateAdapter() + ) } override fun notifyFocusedRect(rect: Rect) { @@ -424,7 +427,7 @@ fun PlatformContextTextInputService.asPlatformTextInputService() = object : Plat decorationBoxBounds: Rect ) { this@asPlatformTextInputService.updateTextLayoutResult( - textFieldValue = textFieldValue, + textFieldValue = textFieldValue.asTextFieldStateAdapter(), offsetMapping = offsetMapping, textLayoutResult = textLayoutResult, textFieldToRootTransform = textFieldToRootTransform, 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 0e572bccba98d..cc145725d33ff 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,10 +16,12 @@ package androidx.compose.ui.platform +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextRange 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 { val state: TextFieldStateAdapter @@ -40,3 +42,39 @@ interface TextFieldStateAdapter { val selection: TextRange val composition: TextRange? } + +/** + * Adapts a [TextFieldValue] to [TextFieldStateAdapter]. + */ +fun TextFieldValue.asTextFieldStateAdapter() = object: TextFieldStateAdapter { + + override val text: CharSequence + get() = this@asTextFieldStateAdapter.annotatedString + + override val selection: TextRange + get() = this@asTextFieldStateAdapter.selection + + override val composition: TextRange? + get() = this@asTextFieldStateAdapter.composition + +} + +/** + * Returns a [TextFieldValue] with the current values of a [TextFieldStateAdapter]. + */ +fun TextFieldStateAdapter.toTextFieldValue(): TextFieldValue { + val text = this.text + return if (text is AnnotatedString) { + TextFieldValue( + annotatedString = text, + selection = this.selection, + composition = this.composition + ) + } else { + TextFieldValue( + text = text.toString(), + selection = this.selection, + composition = this.composition + ) + } +} 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 4dbfa8531bfe7..15a63aee305ba 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 @@ -36,7 +36,6 @@ import androidx.compose.ui.text.input.OffsetMapping import androidx.compose.ui.text.input.SetComposingRegionCommand import androidx.compose.ui.text.input.SetComposingTextCommand import androidx.compose.ui.text.input.SetSelectionCommand -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.asCGRect @@ -123,20 +122,20 @@ internal class UIKitTextInputService( private val mainScope = MainScope() override fun startInput( - value: TextFieldValue, + value: TextFieldStateAdapter, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, - onImeActionPerformed: (ImeAction) -> Unit + onImeActionPerformed: ((ImeAction) -> Unit)? ) { currentInput = CurrentInput(value, onEditCommand) _tempCurrentInputSession = EditProcessor().apply { - reset(value, null) + reset(value.toTextFieldValue(), null) } currentImeOptions = imeOptions currentImeActionHandler = onImeActionPerformed attachIntermediateTextInputView() - textUIView?.input = createSkikoInput(value) + textUIView?.input = createSkikoInput() textUIView?.inputTraits = getUITextInputTraits(imeOptions) showSoftwareKeyboard() @@ -166,7 +165,7 @@ internal class UIKitTextInputService( } } - override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { + override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) { val internalOldValue = _tempCurrentInputSession?.toTextFieldValue() val textChanged = internalOldValue == null || internalOldValue.text != newValue.text val selectionChanged = @@ -177,7 +176,7 @@ internal class UIKitTextInputService( if (selectionChanged) { textUIView?.selectionWillChange() } - _tempCurrentInputSession?.reset(newValue, null) + _tempCurrentInputSession?.reset(newValue.toTextFieldValue(), null) currentInput?.let { input -> input.value = newValue _tempCursorPos = null @@ -203,7 +202,7 @@ internal class UIKitTextInputService( } override fun updateTextLayoutResult( - textFieldValue: TextFieldValue, + textFieldValue: TextFieldStateAdapter, offsetMapping: OffsetMapping, textLayoutResult: TextLayoutResult, textFieldToRootTransform: (Matrix) -> Unit, @@ -291,7 +290,7 @@ internal class UIKitTextInputService( return true } - private fun getState(): TextFieldValue? = currentInput?.value + private fun getState(): TextFieldStateAdapter? = currentInput?.value override fun showMenu( rect: Rect, @@ -360,7 +359,7 @@ internal class UIKitTextInputService( textUIView = null } - private fun createSkikoInput(value: TextFieldValue) = object : IOSSkikoInput { + private fun createSkikoInput() = object : IOSSkikoInput { private var floatingCursorTranslation : Offset? = null @@ -537,7 +536,7 @@ internal class UIKitTextInputService( * Returned value must be in range between 0 and length of text (inclusive). */ override fun positionFromPosition(position: Long, offset: Long): Long { - val text = getState()?.text ?: return 0 + val text = getState()?.text?.toString() ?: return 0 if (position + offset >= text.lastIndex + 1) { return (text.lastIndex + 1).toLong() @@ -569,6 +568,6 @@ internal class UIKitTextInputService( } private data class CurrentInput( - var value: TextFieldValue, + var value: TextFieldStateAdapter, val onEditCommand: (List) -> Unit ) diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/BackingTextArea.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/BackingTextArea.kt index b769b7b32ab14..70cd02e21ce78 100644 --- a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/BackingTextArea.kt +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/BackingTextArea.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeOptions import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.SetComposingTextCommand -import androidx.compose.ui.text.input.TextFieldValue import kotlinx.browser.document import org.w3c.dom.HTMLTextAreaElement import org.w3c.dom.events.Event @@ -39,7 +38,7 @@ import org.w3c.dom.events.KeyboardEventInit internal class BackingTextArea( private val imeOptions: ImeOptions, private val onEditCommand: (List) -> Unit, - private val onImeActionPerformed: (ImeAction) -> Unit, + private val onImeActionPerformed: ((ImeAction) -> Unit)?, private val processKeyboardEvent: (KeyboardEvent) -> Unit ) { private val textArea: HTMLTextAreaElement = createHtmlInput() @@ -123,7 +122,7 @@ internal class BackingTextArea( when (evt.inputType) { "insertLineBreak" -> { if (imeOptions.singleLine) { - onImeActionPerformed(imeOptions.imeAction) + onImeActionPerformed?.invoke(imeOptions.imeAction) } } @@ -175,9 +174,9 @@ internal class BackingTextArea( focus() } - fun updateState(textFieldValue: TextFieldValue) { - textArea.value = textFieldValue.text - textArea.setSelectionRange(textFieldValue.selection.start, textFieldValue.selection.end) + fun updateState(textFieldState: TextFieldStateAdapter) { + textArea.value = textFieldState.text.toString() + textArea.setSelectionRange(textFieldState.selection.start, textFieldState.selection.end) } fun dispose() { 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 9a65b92c111a4..14870ff925afb 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 @@ -20,7 +20,6 @@ import androidx.compose.ui.geometry.Rect 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 internal class WebImeInputService(parentInputService: InputAwareInputService) : PlatformContextTextInputService, InputAwareInputService by parentInputService { @@ -31,16 +30,16 @@ internal class WebImeInputService(parentInputService: InputAwareInputService) : } override fun startInput( - value: TextFieldValue, + value: TextFieldStateAdapter, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, - onImeActionPerformed: (ImeAction) -> Unit + onImeActionPerformed: ((ImeAction) -> Unit)? ) { backingTextArea = BackingTextArea( - imeOptions, - onEditCommand, - onImeActionPerformed, + imeOptions = imeOptions, + onEditCommand = onEditCommand, + onImeActionPerformed = onImeActionPerformed, processKeyboardEvent = this@WebImeInputService::processKeyboardEvent ) backingTextArea?.register() @@ -60,7 +59,7 @@ internal class WebImeInputService(parentInputService: InputAwareInputService) : backingTextArea?.blur() } - override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { + override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) { backingTextArea?.updateState(newValue) } diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebKeyboardInputService.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebKeyboardInputService.kt index 4c8dd97001095..54d6d5eccf6ea 100644 --- a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebKeyboardInputService.kt +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebKeyboardInputService.kt @@ -19,19 +19,17 @@ package androidx.compose.ui.platform 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.PlatformTextInputService -import androidx.compose.ui.text.input.TextFieldValue internal class WebKeyboardInputService : PlatformContextTextInputService { override fun startInput( - value: TextFieldValue, + value: TextFieldStateAdapter, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, - onImeActionPerformed: (ImeAction) -> Unit + onImeActionPerformed: ((ImeAction) -> Unit)? ) = Unit override fun stopInput() = Unit override fun showSoftwareKeyboard() = Unit override fun hideSoftwareKeyboard() = Unit - override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) = Unit + override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) = Unit } \ No newline at end of file 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 a2eee2fd95f57..0eee4e5466de1 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 @@ -21,7 +21,6 @@ import androidx.compose.ui.geometry.Rect 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 import org.w3c.dom.events.KeyboardEvent internal interface InputAwareInputService { @@ -39,10 +38,10 @@ internal abstract class WebTextInputService : PlatformContextTextInputService, I } override fun startInput( - value: TextFieldValue, + value: TextFieldStateAdapter, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, - onImeActionPerformed: (ImeAction) -> Unit + onImeActionPerformed: ((ImeAction) -> Unit)? ) { delegatedService().startInput(value, imeOptions, onEditCommand, onImeActionPerformed) } @@ -59,7 +58,7 @@ internal abstract class WebTextInputService : PlatformContextTextInputService, I delegatedService().hideSoftwareKeyboard() } - override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { + override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) { delegatedService().updateState(oldValue, newValue) } From 94856d32297abec393f49eea6d7e202356910c38 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Wed, 14 Aug 2024 00:58:00 +0300 Subject: [PATCH 05/11] Update ui.api --- compose/ui/ui/api/desktop/ui.api | 42 ++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/compose/ui/ui/api/desktop/ui.api b/compose/ui/ui/api/desktop/ui.api index a67048c834924..badbda5e33bbd 100644 --- a/compose/ui/ui/api/desktop/ui.api +++ b/compose/ui/ui/api/desktop/ui.api @@ -134,10 +134,10 @@ public final class androidx/compose/ui/ComposableSingletons$ImageComposeScene_sk public final class androidx/compose/ui/ComposeScene { public static final field $stable I - public fun (Landroidx/compose/ui/text/input/PlatformTextInputService;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;Lkotlin/jvm/functions/Function0;)V - public synthetic fun (Landroidx/compose/ui/text/input/PlatformTextInputService;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Landroidx/compose/ui/text/input/PlatformTextInputService;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Lkotlin/jvm/functions/Function0;)V - public synthetic fun (Landroidx/compose/ui/text/input/PlatformTextInputService;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Landroidx/compose/ui/platform/PlatformContextTextInputService;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;Lkotlin/jvm/functions/Function0;)V + public synthetic fun (Landroidx/compose/ui/platform/PlatformContextTextInputService;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Landroidx/compose/ui/platform/PlatformContextTextInputService;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Lkotlin/jvm/functions/Function0;)V + public synthetic fun (Landroidx/compose/ui/platform/PlatformContextTextInputService;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;Lkotlin/jvm/functions/Function0;)V public synthetic fun (Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Lkotlin/jvm/functions/Function0;)V @@ -3292,13 +3292,15 @@ public abstract interface class androidx/compose/ui/platform/PlatformContext { public fun getParentFocusManager ()Landroidx/compose/ui/focus/FocusManager; public fun getRootForTestListener ()Landroidx/compose/ui/platform/PlatformContext$RootForTestListener; public fun getSemanticsOwnerListener ()Landroidx/compose/ui/platform/PlatformContext$SemanticsOwnerListener; - public fun getTextInputService ()Landroidx/compose/ui/text/input/PlatformTextInputService; + public fun getTextInputService ()Landroidx/compose/ui/platform/PlatformContextTextInputService; public fun getTextToolbar ()Landroidx/compose/ui/platform/TextToolbar; public fun getViewConfiguration ()Landroidx/compose/ui/platform/ViewConfiguration; public abstract fun getWindowInfo ()Landroidx/compose/ui/platform/WindowInfo; public fun isWindowTransparent ()Z public fun requestFocus ()Z public fun setPointerIcon (Landroidx/compose/ui/input/pointer/PointerIcon;)V + 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 { @@ -3317,6 +3319,21 @@ public abstract interface class androidx/compose/ui/platform/PlatformContext$Sem public abstract fun onSemanticsOwnerRemoved (Landroidx/compose/ui/semantics/SemanticsOwner;)V } +public abstract interface class androidx/compose/ui/platform/PlatformContextTextInputService { + public abstract fun hideSoftwareKeyboard ()V + public fun notifyFocusedRect (Landroidx/compose/ui/geometry/Rect;)V + public abstract fun showSoftwareKeyboard ()V + public fun startInput ()V + public abstract fun startInput (Landroidx/compose/ui/platform/TextFieldStateAdapter;Landroidx/compose/ui/text/input/ImeOptions;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V + public abstract fun stopInput ()V + public abstract fun updateState (Landroidx/compose/ui/platform/TextFieldStateAdapter;Landroidx/compose/ui/platform/TextFieldStateAdapter;)V + public fun updateTextLayoutResult (Landroidx/compose/ui/platform/TextFieldStateAdapter;Landroidx/compose/ui/text/input/OffsetMapping;Landroidx/compose/ui/text/TextLayoutResult;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/geometry/Rect;Landroidx/compose/ui/geometry/Rect;)V +} + +public final class androidx/compose/ui/platform/PlatformContext_skikoKt { + public static final fun asPlatformTextInputService (Landroidx/compose/ui/platform/PlatformContextTextInputService;)Landroidx/compose/ui/text/input/PlatformTextInputService; +} + public abstract interface class androidx/compose/ui/platform/PlatformDragAndDropManager { public abstract fun drag-12SF9DM (Landroidx/compose/ui/draganddrop/DragAndDropTransferData;JLkotlin/jvm/functions/Function1;)Z public abstract fun getModifier ()Landroidx/compose/ui/Modifier; @@ -3367,6 +3384,15 @@ 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/platform/TextFieldStateAdapter; +} + +public final class androidx/compose/ui/platform/PlatformTextInputMethodRequest_skikoKt { + public static final fun asTextFieldStateAdapter (Landroidx/compose/ui/text/input/TextFieldValue;)Landroidx/compose/ui/platform/TextFieldStateAdapter; + public static final fun toTextFieldValue (Landroidx/compose/ui/platform/TextFieldStateAdapter;)Landroidx/compose/ui/text/input/TextFieldValue; } public abstract interface class androidx/compose/ui/platform/PlatformTextInputModifierNode : androidx/compose/ui/node/DelegatableNode { @@ -3393,6 +3419,12 @@ public final class androidx/compose/ui/platform/TestTagKt { public static final fun testTag (Landroidx/compose/ui/Modifier;Ljava/lang/String;)Landroidx/compose/ui/Modifier; } +public abstract interface class androidx/compose/ui/platform/TextFieldStateAdapter { + public abstract fun getComposition-MzsxiRA ()Landroidx/compose/ui/text/TextRange; + public abstract fun getSelection-d9O1mEE ()J + public abstract fun getText ()Ljava/lang/CharSequence; +} + public abstract interface class androidx/compose/ui/platform/TextToolbar { public abstract fun getStatus ()Landroidx/compose/ui/platform/TextToolbarStatus; public abstract fun hide ()V From e249584446218e4c72be1e89a0f4a17642c1dc2a Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Fri, 16 Aug 2024 11:23:39 +0300 Subject: [PATCH 06/11] Always assign `UIKitTextInputService.currentImeActionHandler` a non-null value during a text input session. --- .../androidx/compose/ui/platform/UIKitTextInputService.uikit.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 15a63aee305ba..d59d0c7b2f0e3 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 @@ -132,7 +132,7 @@ internal class UIKitTextInputService( reset(value.toTextFieldValue(), null) } currentImeOptions = imeOptions - currentImeActionHandler = onImeActionPerformed + currentImeActionHandler = onImeActionPerformed ?: { } attachIntermediateTextInputView() textUIView?.input = createSkikoInput() From 3b572cdd7dcbbabdeb2dc1b10ddf4f8692912781 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Sun, 18 Aug 2024 14:01:40 +0300 Subject: [PATCH 07/11] Move `PlatformContextTextInputService` to a separate file. --- compose/ui/ui/api/desktop/ui.api | 4 - .../ui/platform/PlatformContext.skiko.kt | 137 -------------- .../PlatformContextTextInputService.kt | 169 ++++++++++++++++++ 3 files changed, 169 insertions(+), 141 deletions(-) create mode 100644 compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContextTextInputService.kt diff --git a/compose/ui/ui/api/desktop/ui.api b/compose/ui/ui/api/desktop/ui.api index badbda5e33bbd..90c8b5af1f31d 100644 --- a/compose/ui/ui/api/desktop/ui.api +++ b/compose/ui/ui/api/desktop/ui.api @@ -3330,10 +3330,6 @@ public abstract interface class androidx/compose/ui/platform/PlatformContextText public fun updateTextLayoutResult (Landroidx/compose/ui/platform/TextFieldStateAdapter;Landroidx/compose/ui/text/input/OffsetMapping;Landroidx/compose/ui/text/TextLayoutResult;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/geometry/Rect;Landroidx/compose/ui/geometry/Rect;)V } -public final class androidx/compose/ui/platform/PlatformContext_skikoKt { - public static final fun asPlatformTextInputService (Landroidx/compose/ui/platform/PlatformContextTextInputService;)Landroidx/compose/ui/text/input/PlatformTextInputService; -} - public abstract interface class androidx/compose/ui/platform/PlatformDragAndDropManager { public abstract fun drag-12SF9DM (Landroidx/compose/ui/draganddrop/DragAndDropTransferData;JLkotlin/jvm/functions/Function1;)Z public abstract fun getModifier ()Landroidx/compose/ui/Modifier; 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 f52efef9ec49b..0da028e9420d6 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 @@ -299,140 +299,3 @@ internal class DelegateRootForTestListener : PlatformContext.RootForTestListener } } -/** - * Platform specific text input service. - * - * This is a non-deprecated version of [PlatformTextInputService], which is needed because we can't - * expose deprecated APIs in [PlatformContext]. - */ -interface PlatformContextTextInputService { - /** - * Start text input session for given client. - * - * @see TextInputService.startInput - */ - fun startInput( - value: TextFieldStateAdapter, - imeOptions: ImeOptions, - onEditCommand: (List) -> Unit, - onImeActionPerformed: ((ImeAction) -> Unit)? - ) - - /** - * Restart input and show the keyboard. This should only be called when starting a new - * `PlatformTextInputModifierNode.textInputSession`. - * - * @see TextInputService.startInput - */ - fun startInput() {} - - /** - * Stop text input session. - * - * @see TextInputService.stopInput - */ - fun stopInput() - - /** - * Request showing onscreen keyboard - * - * There is no guarantee nor callback of the result of this API. - * - * @see TextInputService.showSoftwareKeyboard - */ - fun showSoftwareKeyboard() - - /** - * Hide software keyboard - * - * @see TextInputService.hideSoftwareKeyboard - */ - fun hideSoftwareKeyboard() - - /** - * Notify the new editor model to IME. - * - * @see TextInputSession.updateState - */ - fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) - - /** - * Notify the focused rectangle to the system. - * - * The system can ignore this information or use it to for additional functionality. - * - * For example, desktop systems show a popup near the focused input area (for some languages). - */ - // TODO(b/262648050) Try to find a better API. - fun notifyFocusedRect(rect: Rect) { - } - - /** - * Notify the input service of layout and position changes. - * - * @see TextInputSession.updateTextLayoutResult - */ - fun updateTextLayoutResult( - textFieldValue: TextFieldStateAdapter, - offsetMapping: OffsetMapping, - textLayoutResult: TextLayoutResult, - textFieldToRootTransform: (Matrix) -> Unit, - innerTextFieldBounds: Rect, - decorationBoxBounds: Rect - ) { - } -} - -@Suppress("DEPRECATION") -fun PlatformContextTextInputService.asPlatformTextInputService() = object : PlatformTextInputService { - override fun startInput( - value: TextFieldValue, - imeOptions: ImeOptions, - onEditCommand: (List) -> Unit, - onImeActionPerformed: (ImeAction) -> Unit - ) { - this@asPlatformTextInputService.startInput( - value = value.asTextFieldStateAdapter(), - imeOptions = imeOptions, - onEditCommand = onEditCommand, - onImeActionPerformed = onImeActionPerformed - ) - } - - override fun startInput() = this@asPlatformTextInputService.startInput() - - override fun stopInput() = this@asPlatformTextInputService.stopInput() - - override fun showSoftwareKeyboard() = this@asPlatformTextInputService.showSoftwareKeyboard() - - override fun hideSoftwareKeyboard() = this@asPlatformTextInputService.hideSoftwareKeyboard() - - override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { - this@asPlatformTextInputService.updateState( - oldValue = oldValue?.asTextFieldStateAdapter(), - newValue = newValue.asTextFieldStateAdapter() - ) - } - - override fun notifyFocusedRect(rect: Rect) { - this@asPlatformTextInputService.notifyFocusedRect(rect) - } - - override fun updateTextLayoutResult( - textFieldValue: TextFieldValue, - offsetMapping: OffsetMapping, - textLayoutResult: TextLayoutResult, - textFieldToRootTransform: (Matrix) -> Unit, - innerTextFieldBounds: Rect, - decorationBoxBounds: Rect - ) { - this@asPlatformTextInputService.updateTextLayoutResult( - textFieldValue = textFieldValue.asTextFieldStateAdapter(), - offsetMapping = offsetMapping, - textLayoutResult = textLayoutResult, - textFieldToRootTransform = textFieldToRootTransform, - innerTextFieldBounds = innerTextFieldBounds, - decorationBoxBounds = decorationBoxBounds - ) - } -} diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContextTextInputService.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContextTextInputService.kt new file mode 100644 index 0000000000000..0bc0008f7ad2d --- /dev/null +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContextTextInputService.kt @@ -0,0 +1,169 @@ +/* + * Copyright 2024 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. + * You may obtain a copy of the License at + * + * http://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. + */ + +package androidx.compose.ui.platform + +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Matrix +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 + + +/** + * Platform specific text input service. + * + * This is a non-deprecated version of [PlatformTextInputService], which is needed because we can't + * expose deprecated APIs in [PlatformContext]. + */ +interface PlatformContextTextInputService { + /** + * Start text input session for given client. + * + * @see TextInputService.startInput + */ + fun startInput( + value: TextFieldStateAdapter, + imeOptions: ImeOptions, + onEditCommand: (List) -> Unit, + onImeActionPerformed: ((ImeAction) -> Unit)? + ) + + /** + * Restart input and show the keyboard. This should only be called when starting a new + * `PlatformTextInputModifierNode.textInputSession`. + * + * @see TextInputService.startInput + */ + fun startInput() {} + + /** + * Stop text input session. + * + * @see TextInputService.stopInput + */ + fun stopInput() + + /** + * Request showing onscreen keyboard + * + * There is no guarantee nor callback of the result of this API. + * + * @see TextInputService.showSoftwareKeyboard + */ + fun showSoftwareKeyboard() + + /** + * Hide software keyboard + * + * @see TextInputService.hideSoftwareKeyboard + */ + fun hideSoftwareKeyboard() + + /** + * Notify the new editor model to IME. + * + * @see TextInputSession.updateState + */ + fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) + + /** + * Notify the focused rectangle to the system. + * + * The system can ignore this information or use it to for additional functionality. + * + * For example, desktop systems show a popup near the focused input area (for some languages). + */ + // TODO(b/262648050) Try to find a better API. + fun notifyFocusedRect(rect: Rect) { + } + + /** + * Notify the input service of layout and position changes. + * + * @see TextInputSession.updateTextLayoutResult + */ + fun updateTextLayoutResult( + textFieldValue: TextFieldStateAdapter, + offsetMapping: OffsetMapping, + textLayoutResult: TextLayoutResult, + textFieldToRootTransform: (Matrix) -> Unit, + innerTextFieldBounds: Rect, + decorationBoxBounds: Rect + ) { + } +} + +@Suppress("DEPRECATION") +internal fun PlatformContextTextInputService.asPlatformTextInputService() = object : + PlatformTextInputService { + override fun startInput( + value: TextFieldValue, + imeOptions: ImeOptions, + onEditCommand: (List) -> Unit, + onImeActionPerformed: (ImeAction) -> Unit + ) { + this@asPlatformTextInputService.startInput( + value = value.asTextFieldStateAdapter(), + imeOptions = imeOptions, + onEditCommand = onEditCommand, + onImeActionPerformed = onImeActionPerformed + ) + } + + override fun startInput() = this@asPlatformTextInputService.startInput() + + override fun stopInput() = this@asPlatformTextInputService.stopInput() + + override fun showSoftwareKeyboard() = this@asPlatformTextInputService.showSoftwareKeyboard() + + override fun hideSoftwareKeyboard() = this@asPlatformTextInputService.hideSoftwareKeyboard() + + override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { + this@asPlatformTextInputService.updateState( + oldValue = oldValue?.asTextFieldStateAdapter(), + newValue = newValue.asTextFieldStateAdapter() + ) + } + + override fun notifyFocusedRect(rect: Rect) { + this@asPlatformTextInputService.notifyFocusedRect(rect) + } + + override fun updateTextLayoutResult( + textFieldValue: TextFieldValue, + offsetMapping: OffsetMapping, + textLayoutResult: TextLayoutResult, + textFieldToRootTransform: (Matrix) -> Unit, + innerTextFieldBounds: Rect, + decorationBoxBounds: Rect + ) { + this@asPlatformTextInputService.updateTextLayoutResult( + textFieldValue = textFieldValue.asTextFieldStateAdapter(), + offsetMapping = offsetMapping, + textLayoutResult = textLayoutResult, + textFieldToRootTransform = textFieldToRootTransform, + innerTextFieldBounds = innerTextFieldBounds, + decorationBoxBounds = decorationBoxBounds + ) + } +} From 00fba50db592f955c5a8bc8cf4ecda3d06ae5767 Mon Sep 17 00:00:00 2001 From: Alexander Maryanovsky Date: Mon, 19 Aug 2024 11:47:09 +0300 Subject: [PATCH 08/11] Fix text old vs. new text comparison in UIKitTextInputService.updateState --- .../androidx/compose/ui/test/ComposeUiTest.skikoMain.kt | 2 +- .../compose/ui/keyboard/KeyboardInsetsTest.kt | 2 +- .../compose/ui/test/UIKitInstrumentedTest.kt | 0 .../compose/ui/platform/UIKitTextInputService.uikit.kt | 6 ++---- 4 files changed, 4 insertions(+), 6 deletions(-) rename compose/ui/ui/src/uikitInstrumentedTest/kotlin/{android => androidx}/compose/ui/keyboard/KeyboardInsetsTest.kt (99%) rename compose/ui/ui/src/uikitInstrumentedTest/kotlin/{android => androidx}/compose/ui/test/UIKitInstrumentedTest.kt (100%) 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 e406402ddaada..4df3031745515 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 @@ -147,7 +147,7 @@ class SkikoComposeUiTest @InternalTestApi constructor( private class Session( var imeOptions: ImeOptions, var onEditCommand: (List) -> Unit, - var onImeActionPerformed: (ImeAction) -> Unit, + var onImeActionPerformed: ((ImeAction) -> Unit)?, ) private val composeRootRegistry = ComposeRootRegistry() 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 d59d0c7b2f0e3..9fd8f1ceac1dc 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 @@ -167,7 +167,7 @@ internal class UIKitTextInputService( override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) { val internalOldValue = _tempCurrentInputSession?.toTextFieldValue() - val textChanged = internalOldValue == null || internalOldValue.text != newValue.text + val textChanged = internalOldValue == null || internalOldValue.text != newValue.text.toString() val selectionChanged = textChanged || internalOldValue == null || internalOldValue.selection != newValue.selection if (textChanged) { @@ -246,9 +246,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? { From 7b857c5f7effd740d04e4b324aade88f363cc693 Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Tue, 27 Aug 2024 21:34:16 +0200 Subject: [PATCH 09/11] Remove PlatformContextTextInputService, undeprecate PlatformTextInputService --- .../input/internal/TextInputSession.skiko.kt | 26 +-- .../ui/test/ComposeUiTest.skikoMain.kt | 16 +- .../compose/ui/text/input/TextInputService.kt | 2 - .../platform/DesktopPlatformInput.desktop.kt | 16 +- .../ui/scene/ComposeSceneMediator.desktop.kt | 2 +- .../ui/platform/DesktopInputComponentTest.kt | 4 +- .../platform/MacosTextInputService.macos.kt | 12 +- .../androidx/compose/ui/ComposeScene.skiko.kt | 5 +- .../compose/ui/node/RootNodeOwner.skiko.kt | 3 +- .../ui/platform/PlatformContext.skiko.kt | 10 +- .../PlatformContextTextInputService.kt | 169 ------------------ .../PlatformTextInputMethodRequest.skiko.kt | 53 +----- .../platform/UIKitTextInputService.uikit.kt | 26 +-- .../compose/ui/platform/BackingTextArea.kt | 11 +- .../compose/ui/platform/WebImeInputService.kt | 10 +- .../ui/platform/WebKeyboardInputService.kt | 10 +- .../ui/platform/WebTextInputService.kt | 12 +- 17 files changed, 82 insertions(+), 305 deletions(-) delete mode 100644 compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContextTextInputService.kt 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 faa586db7971e..49bc295da843c 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 @@ -19,9 +19,7 @@ package androidx.compose.foundation.text.input.internal import androidx.compose.foundation.content.internal.ReceiveContentConfiguration import androidx.compose.ui.platform.PlatformTextInputMethodRequest import androidx.compose.ui.platform.PlatformTextInputSession -import androidx.compose.ui.platform.TextFieldStateAdapter import androidx.compose.ui.platform.ViewConfiguration -import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.EditCommand import androidx.compose.ui.text.input.EditProcessor import androidx.compose.ui.text.input.ImeAction @@ -70,7 +68,11 @@ internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSe startInputMethod( SkikoPlatformTextInputMethodRequest( - state = TransformedTextFieldStateAdapter(state), + state = TextFieldValue( + state.visualText.toString(), + state.visualText.selection, + state.visualText.composition, + ), imeOptions = imeOptions, onEditCommand = ::onEditCommand, onImeAction = onImeAction @@ -78,24 +80,8 @@ internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSe ) } - -private class TransformedTextFieldStateAdapter( - val state: TransformedTextFieldState -) : TextFieldStateAdapter { - - override val text: CharSequence - get() = state.visualText - - override val selection: TextRange - get() = state.visualText.selection - - override val composition: TextRange? - get() = state.visualText.composition - -} - private data class SkikoPlatformTextInputMethodRequest( - override val state: TextFieldStateAdapter, + override val state: TextFieldValue, override val imeOptions: ImeOptions, override val onEditCommand: (List) -> Unit, override val onImeAction: ((ImeAction) -> Unit)? 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 4df3031745515..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 @@ -27,8 +27,6 @@ import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.node.RootForTest import androidx.compose.ui.platform.InfiniteAnimationPolicy import androidx.compose.ui.platform.PlatformContext -import androidx.compose.ui.platform.PlatformContextTextInputService -import androidx.compose.ui.platform.TextFieldStateAdapter import androidx.compose.ui.platform.WindowInfo import androidx.compose.ui.scene.ComposeScene import androidx.compose.ui.scene.ComposeSceneContext @@ -37,6 +35,8 @@ import androidx.compose.ui.semantics.SemanticsNode 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.PlatformTextInputService +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import kotlin.coroutines.CoroutineContext @@ -147,7 +147,7 @@ class SkikoComposeUiTest @InternalTestApi constructor( private class Session( var imeOptions: ImeOptions, var onEditCommand: (List) -> Unit, - var onImeActionPerformed: ((ImeAction) -> Unit)?, + var onImeActionPerformed: (ImeAction) -> Unit, ) private val composeRootRegistry = ComposeRootRegistry() @@ -391,19 +391,19 @@ class SkikoComposeUiTest @InternalTestApi constructor( get() = size } - private inner class TestTextInputService : PlatformContextTextInputService { + private inner class TestTextInputService : PlatformTextInputService { var session: Session? = null override fun startInput( - value: TextFieldStateAdapter, + value: TextFieldValue, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, - onImeActionPerformed: ((ImeAction) -> Unit)? + onImeActionPerformed: (ImeAction) -> Unit ) { session = Session( imeOptions = imeOptions, onEditCommand = onEditCommand, - onImeActionPerformed = onImeActionPerformed ?: { } + onImeActionPerformed = onImeActionPerformed ) } @@ -413,7 +413,7 @@ class SkikoComposeUiTest @InternalTestApi constructor( override fun showSoftwareKeyboard() = Unit override fun hideSoftwareKeyboard() = Unit - override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) = Unit + override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) = Unit } private inner class TestContext : PlatformContext by PlatformContext.Empty { 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/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 46af900ca4660..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 @@ -22,7 +22,9 @@ import androidx.compose.ui.text.input.DeleteSurroundingTextInCodePointsCommand 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.PlatformTextInputService import androidx.compose.ui.text.input.SetComposingTextCommand +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.substring import java.awt.Rectangle import java.awt.event.InputMethodEvent @@ -32,16 +34,16 @@ import java.awt.im.InputMethodRequests import java.text.AttributedCharacterIterator import java.text.AttributedString import java.text.CharacterIterator -import java.util.* +import java.util.Locale import kotlin.math.max import kotlin.math.min internal class DesktopTextInputService(private val component: PlatformComponent) : - PlatformContextTextInputService { + PlatformTextInputService { data class CurrentInput( - var value: TextFieldStateAdapter, + var value: TextFieldValue, val onEditCommand: ((List) -> Unit), - val onImeActionPerformed: ((ImeAction) -> Unit)?, + val onImeActionPerformed: (ImeAction) -> Unit, val imeAction: ImeAction, var focusedRect: Rect? = null ) @@ -56,10 +58,10 @@ internal class DesktopTextInputService(private val component: PlatformComponent) var needToDeletePreviousChar: Boolean = false override fun startInput( - value: TextFieldStateAdapter, + value: TextFieldValue, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, - onImeActionPerformed: ((ImeAction) -> Unit)? + onImeActionPerformed: (ImeAction) -> Unit ) { val input = CurrentInput( value = value, @@ -83,7 +85,7 @@ internal class DesktopTextInputService(private val component: PlatformComponent) override fun hideSoftwareKeyboard() { } - override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) { + override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { currentInput?.value = newValue } 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 6332b6eee2623..3125352732de0 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 @@ -752,7 +752,7 @@ internal class ComposeSceneMediator( value = request.state, imeOptions = request.imeOptions, onEditCommand = request.onEditCommand, - onImeActionPerformed = request.onImeAction + onImeActionPerformed = request.onImeAction ?: {} ) continuation.invokeOnCancellation { diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt index 8249a06db6a78..be403e1ecb0b1 100644 --- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt +++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt @@ -41,7 +41,7 @@ class DesktopInputComponentTest { val processor = EditProcessor() val input = DesktopTextInputService(PlatformComponent.Empty) - val inputService = TextInputService(input.asPlatformTextInputService()) + val inputService = TextInputService(input) val session = inputService.startInput( value = TextFieldValue(), @@ -88,7 +88,7 @@ class DesktopInputComponentTest { } } val input = DesktopTextInputService(component) - val inputService = TextInputService(input.asPlatformTextInputService()) + val inputService = TextInputService(input) val session = inputService.startInput( value = TextFieldValue(), diff --git a/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/platform/MacosTextInputService.macos.kt b/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/platform/MacosTextInputService.macos.kt index 76019b8c42ccc..db7ad374c71d8 100644 --- a/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/platform/MacosTextInputService.macos.kt +++ b/compose/ui/ui/src/macosMain/kotlin/androidx/compose/ui/platform/MacosTextInputService.macos.kt @@ -19,21 +19,23 @@ package androidx.compose.ui.platform 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.PlatformTextInputService +import androidx.compose.ui.text.input.TextFieldValue -internal class MacosTextInputService : PlatformContextTextInputService { +internal class MacosTextInputService : PlatformTextInputService { data class CurrentInput( - var value: TextFieldStateAdapter, + var value: TextFieldValue, val onEditCommand: ((List) -> Unit), ) private var currentInput: CurrentInput? = null override fun startInput( - value: TextFieldStateAdapter, + value: TextFieldValue, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, - onImeActionPerformed: ((ImeAction) -> Unit)? + onImeActionPerformed: (ImeAction) -> Unit ) { currentInput = CurrentInput( value, @@ -53,7 +55,7 @@ internal class MacosTextInputService : PlatformContextTextInputService { //do nothing } - override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) { + override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { currentInput?.let { input -> input.value = newValue } diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt index dff6d0f0b94dd..0742e946a381d 100644 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt +++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ComposeScene.skiko.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.node.RootForTest import androidx.compose.ui.platform.* import androidx.compose.ui.scene.ComposeSceneContext import androidx.compose.ui.scene.ComposeScenePointer +import androidx.compose.ui.text.input.PlatformTextInputService import androidx.compose.ui.unit.* import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.* @@ -97,7 +98,7 @@ class ComposeScene internal constructor( */ @ExperimentalComposeUiApi constructor( - textInputService: PlatformContextTextInputService, + textInputService: PlatformTextInputService, coroutineContext: CoroutineContext = Dispatchers.Unconfined, density: Density = Density(1f), layoutDirection: LayoutDirection = LayoutDirection.Ltr, @@ -148,7 +149,7 @@ class ComposeScene internal constructor( * schedule the next [render] in your rendering loop. */ constructor( - textInputService: PlatformContextTextInputService, + textInputService: PlatformTextInputService, coroutineContext: CoroutineContext = Dispatchers.Unconfined, density: Density = Density(1f), invalidate: () -> Unit = {} 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 4d50175d7f334..658e98f2024f4 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 @@ -66,7 +66,6 @@ import androidx.compose.ui.platform.PlatformRootForTest import androidx.compose.ui.platform.PlatformTextInputSessionScope import androidx.compose.ui.platform.RenderNodeLayer import androidx.compose.ui.platform.asDragAndDropManager -import androidx.compose.ui.platform.asPlatformTextInputService import androidx.compose.ui.scene.ComposeScene import androidx.compose.ui.scene.ComposeSceneInputHandler import androidx.compose.ui.scene.ComposeScenePointer @@ -319,7 +318,7 @@ internal class RootNodeOwner( override val autofill: Autofill? get() = null override val density get() = this@RootNodeOwner.density override val textInputService = - TextInputService(platformContext.textInputService.asPlatformTextInputService()) + TextInputService(platformContext.textInputService) override val softwareKeyboardController = DelegatingSoftwareKeyboardController(textInputService) 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 0da028e9420d6..8b0df62d1ea68 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 @@ -114,7 +114,7 @@ interface PlatformContext { val viewConfiguration: ViewConfiguration get() = EmptyViewConfiguration val inputModeManager: InputModeManager - val textInputService: PlatformContextTextInputService get() = EmptyPlatformContextTextInputService + val textInputService: PlatformTextInputService get() = EmptyPlatformTextInputService suspend fun textInputSession( session: suspend PlatformTextInputSessionScope.() -> Nothing @@ -217,18 +217,18 @@ internal object EmptyViewConfiguration : ViewConfiguration { override val touchSlop: Float = 18f } -private object EmptyPlatformContextTextInputService : PlatformContextTextInputService { +private object EmptyPlatformTextInputService : PlatformTextInputService { override fun startInput( - value: TextFieldStateAdapter, + value: TextFieldValue, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, - onImeActionPerformed: ((ImeAction) -> Unit)? + onImeActionPerformed: (ImeAction) -> Unit ) = Unit override fun stopInput() = Unit override fun showSoftwareKeyboard() = Unit override fun hideSoftwareKeyboard() = Unit - override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) = Unit + override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) = Unit } private object EmptyTextToolbar : TextToolbar { diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContextTextInputService.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContextTextInputService.kt deleted file mode 100644 index 0bc0008f7ad2d..0000000000000 --- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformContextTextInputService.kt +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright 2024 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. - * You may obtain a copy of the License at - * - * http://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. - */ - -package androidx.compose.ui.platform - -import androidx.compose.ui.geometry.Rect -import androidx.compose.ui.graphics.Matrix -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 - - -/** - * Platform specific text input service. - * - * This is a non-deprecated version of [PlatformTextInputService], which is needed because we can't - * expose deprecated APIs in [PlatformContext]. - */ -interface PlatformContextTextInputService { - /** - * Start text input session for given client. - * - * @see TextInputService.startInput - */ - fun startInput( - value: TextFieldStateAdapter, - imeOptions: ImeOptions, - onEditCommand: (List) -> Unit, - onImeActionPerformed: ((ImeAction) -> Unit)? - ) - - /** - * Restart input and show the keyboard. This should only be called when starting a new - * `PlatformTextInputModifierNode.textInputSession`. - * - * @see TextInputService.startInput - */ - fun startInput() {} - - /** - * Stop text input session. - * - * @see TextInputService.stopInput - */ - fun stopInput() - - /** - * Request showing onscreen keyboard - * - * There is no guarantee nor callback of the result of this API. - * - * @see TextInputService.showSoftwareKeyboard - */ - fun showSoftwareKeyboard() - - /** - * Hide software keyboard - * - * @see TextInputService.hideSoftwareKeyboard - */ - fun hideSoftwareKeyboard() - - /** - * Notify the new editor model to IME. - * - * @see TextInputSession.updateState - */ - fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) - - /** - * Notify the focused rectangle to the system. - * - * The system can ignore this information or use it to for additional functionality. - * - * For example, desktop systems show a popup near the focused input area (for some languages). - */ - // TODO(b/262648050) Try to find a better API. - fun notifyFocusedRect(rect: Rect) { - } - - /** - * Notify the input service of layout and position changes. - * - * @see TextInputSession.updateTextLayoutResult - */ - fun updateTextLayoutResult( - textFieldValue: TextFieldStateAdapter, - offsetMapping: OffsetMapping, - textLayoutResult: TextLayoutResult, - textFieldToRootTransform: (Matrix) -> Unit, - innerTextFieldBounds: Rect, - decorationBoxBounds: Rect - ) { - } -} - -@Suppress("DEPRECATION") -internal fun PlatformContextTextInputService.asPlatformTextInputService() = object : - PlatformTextInputService { - override fun startInput( - value: TextFieldValue, - imeOptions: ImeOptions, - onEditCommand: (List) -> Unit, - onImeActionPerformed: (ImeAction) -> Unit - ) { - this@asPlatformTextInputService.startInput( - value = value.asTextFieldStateAdapter(), - imeOptions = imeOptions, - onEditCommand = onEditCommand, - onImeActionPerformed = onImeActionPerformed - ) - } - - override fun startInput() = this@asPlatformTextInputService.startInput() - - override fun stopInput() = this@asPlatformTextInputService.stopInput() - - override fun showSoftwareKeyboard() = this@asPlatformTextInputService.showSoftwareKeyboard() - - override fun hideSoftwareKeyboard() = this@asPlatformTextInputService.hideSoftwareKeyboard() - - override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { - this@asPlatformTextInputService.updateState( - oldValue = oldValue?.asTextFieldStateAdapter(), - newValue = newValue.asTextFieldStateAdapter() - ) - } - - override fun notifyFocusedRect(rect: Rect) { - this@asPlatformTextInputService.notifyFocusedRect(rect) - } - - override fun updateTextLayoutResult( - textFieldValue: TextFieldValue, - offsetMapping: OffsetMapping, - textLayoutResult: TextLayoutResult, - textFieldToRootTransform: (Matrix) -> Unit, - innerTextFieldBounds: Rect, - decorationBoxBounds: Rect - ) { - this@asPlatformTextInputService.updateTextLayoutResult( - textFieldValue = textFieldValue.asTextFieldStateAdapter(), - offsetMapping = offsetMapping, - textLayoutResult = textLayoutResult, - textFieldToRootTransform = textFieldToRootTransform, - innerTextFieldBounds = innerTextFieldBounds, - decorationBoxBounds = decorationBoxBounds - ) - } -} 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 cc145725d33ff..3ee3ac5dfb093 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,65 +16,14 @@ package androidx.compose.ui.platform -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.TextRange 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 { - val state: TextFieldStateAdapter + val state: TextFieldValue val imeOptions: ImeOptions val onEditCommand: (List) -> Unit val onImeAction: ((ImeAction) -> Unit)? } - -/** - * The purpose of this interface is to provide, in the `ui` module, an adapter for - * `TransformedTextFieldStateAdapter`, which is in the `foundation` module. - * - * It exposes all the properties of `TransformedTextFieldStateAdapter` that are necessary for - * implementing input methods. - */ -interface TextFieldStateAdapter { - val text: CharSequence - val selection: TextRange - val composition: TextRange? -} - -/** - * Adapts a [TextFieldValue] to [TextFieldStateAdapter]. - */ -fun TextFieldValue.asTextFieldStateAdapter() = object: TextFieldStateAdapter { - - override val text: CharSequence - get() = this@asTextFieldStateAdapter.annotatedString - - override val selection: TextRange - get() = this@asTextFieldStateAdapter.selection - - override val composition: TextRange? - get() = this@asTextFieldStateAdapter.composition - -} - -/** - * Returns a [TextFieldValue] with the current values of a [TextFieldStateAdapter]. - */ -fun TextFieldStateAdapter.toTextFieldValue(): TextFieldValue { - val text = this.text - return if (text is AnnotatedString) { - TextFieldValue( - annotatedString = text, - selection = this.selection, - composition = this.composition - ) - } else { - TextFieldValue( - text = text.toString(), - selection = this.selection, - composition = this.composition - ) - } -} 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 9fd8f1ceac1dc..dc3b14f85e5c2 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 @@ -33,9 +33,11 @@ import androidx.compose.ui.text.input.FinishComposingTextCommand 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.SetComposingRegionCommand import androidx.compose.ui.text.input.SetComposingTextCommand import androidx.compose.ui.text.input.SetSelectionCommand +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.asCGRect @@ -64,7 +66,7 @@ internal class UIKitTextInputService( * Erasure happens due to K/N not supporting Obj-C lightweight generics. */ private val onKeyboardPresses: (Set<*>) -> Unit, -) : PlatformContextTextInputService, TextToolbar { +) : PlatformTextInputService, TextToolbar { private val rootView get() = rootViewProvider() private var currentInput: CurrentInput? = null @@ -122,17 +124,17 @@ internal class UIKitTextInputService( private val mainScope = MainScope() override fun startInput( - value: TextFieldStateAdapter, + value: TextFieldValue, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, - onImeActionPerformed: ((ImeAction) -> Unit)? + onImeActionPerformed: (ImeAction) -> Unit ) { currentInput = CurrentInput(value, onEditCommand) _tempCurrentInputSession = EditProcessor().apply { - reset(value.toTextFieldValue(), null) + reset(value, null) } currentImeOptions = imeOptions - currentImeActionHandler = onImeActionPerformed ?: { } + currentImeActionHandler = onImeActionPerformed attachIntermediateTextInputView() textUIView?.input = createSkikoInput() @@ -165,9 +167,9 @@ internal class UIKitTextInputService( } } - override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) { + override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { val internalOldValue = _tempCurrentInputSession?.toTextFieldValue() - val textChanged = internalOldValue == null || internalOldValue.text != newValue.text.toString() + val textChanged = internalOldValue == null || internalOldValue.text != newValue.text val selectionChanged = textChanged || internalOldValue == null || internalOldValue.selection != newValue.selection if (textChanged) { @@ -176,7 +178,7 @@ internal class UIKitTextInputService( if (selectionChanged) { textUIView?.selectionWillChange() } - _tempCurrentInputSession?.reset(newValue.toTextFieldValue(), null) + _tempCurrentInputSession?.reset(newValue, null) currentInput?.let { input -> input.value = newValue _tempCursorPos = null @@ -202,7 +204,7 @@ internal class UIKitTextInputService( } override fun updateTextLayoutResult( - textFieldValue: TextFieldStateAdapter, + textFieldValue: TextFieldValue, offsetMapping: OffsetMapping, textLayoutResult: TextLayoutResult, textFieldToRootTransform: (Matrix) -> Unit, @@ -288,7 +290,7 @@ internal class UIKitTextInputService( return true } - private fun getState(): TextFieldStateAdapter? = currentInput?.value + private fun getState(): TextFieldValue? = currentInput?.value override fun showMenu( rect: Rect, @@ -534,7 +536,7 @@ internal class UIKitTextInputService( * Returned value must be in range between 0 and length of text (inclusive). */ override fun positionFromPosition(position: Long, offset: Long): Long { - val text = getState()?.text?.toString() ?: return 0 + val text = getState()?.text ?: return 0 if (position + offset >= text.lastIndex + 1) { return (text.lastIndex + 1).toLong() @@ -566,6 +568,6 @@ internal class UIKitTextInputService( } private data class CurrentInput( - var value: TextFieldStateAdapter, + var value: TextFieldValue, val onEditCommand: (List) -> Unit ) diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/BackingTextArea.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/BackingTextArea.kt index 70cd02e21ce78..b769b7b32ab14 100644 --- a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/BackingTextArea.kt +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/BackingTextArea.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeOptions import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.SetComposingTextCommand +import androidx.compose.ui.text.input.TextFieldValue import kotlinx.browser.document import org.w3c.dom.HTMLTextAreaElement import org.w3c.dom.events.Event @@ -38,7 +39,7 @@ import org.w3c.dom.events.KeyboardEventInit internal class BackingTextArea( private val imeOptions: ImeOptions, private val onEditCommand: (List) -> Unit, - private val onImeActionPerformed: ((ImeAction) -> Unit)?, + private val onImeActionPerformed: (ImeAction) -> Unit, private val processKeyboardEvent: (KeyboardEvent) -> Unit ) { private val textArea: HTMLTextAreaElement = createHtmlInput() @@ -122,7 +123,7 @@ internal class BackingTextArea( when (evt.inputType) { "insertLineBreak" -> { if (imeOptions.singleLine) { - onImeActionPerformed?.invoke(imeOptions.imeAction) + onImeActionPerformed(imeOptions.imeAction) } } @@ -174,9 +175,9 @@ internal class BackingTextArea( focus() } - fun updateState(textFieldState: TextFieldStateAdapter) { - textArea.value = textFieldState.text.toString() - textArea.setSelectionRange(textFieldState.selection.start, textFieldState.selection.end) + fun updateState(textFieldValue: TextFieldValue) { + textArea.value = textFieldValue.text + textArea.setSelectionRange(textFieldValue.selection.start, textFieldValue.selection.end) } fun dispose() { 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 14870ff925afb..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 @@ -20,8 +20,10 @@ import androidx.compose.ui.geometry.Rect 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.PlatformTextInputService +import androidx.compose.ui.text.input.TextFieldValue -internal class WebImeInputService(parentInputService: InputAwareInputService) : PlatformContextTextInputService, InputAwareInputService by parentInputService { +internal class WebImeInputService(parentInputService: InputAwareInputService) : PlatformTextInputService, InputAwareInputService by parentInputService { private var backingTextArea: BackingTextArea? = null set(value) { @@ -30,10 +32,10 @@ internal class WebImeInputService(parentInputService: InputAwareInputService) : } override fun startInput( - value: TextFieldStateAdapter, + value: TextFieldValue, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, - onImeActionPerformed: ((ImeAction) -> Unit)? + onImeActionPerformed: (ImeAction) -> Unit ) { backingTextArea = BackingTextArea( @@ -59,7 +61,7 @@ internal class WebImeInputService(parentInputService: InputAwareInputService) : backingTextArea?.blur() } - override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) { + override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { backingTextArea?.updateState(newValue) } diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebKeyboardInputService.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebKeyboardInputService.kt index 54d6d5eccf6ea..1b99719669887 100644 --- a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebKeyboardInputService.kt +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/platform/WebKeyboardInputService.kt @@ -19,17 +19,19 @@ package androidx.compose.ui.platform 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.PlatformTextInputService +import androidx.compose.ui.text.input.TextFieldValue -internal class WebKeyboardInputService : PlatformContextTextInputService { +internal class WebKeyboardInputService : PlatformTextInputService { override fun startInput( - value: TextFieldStateAdapter, + value: TextFieldValue, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, - onImeActionPerformed: ((ImeAction) -> Unit)? + onImeActionPerformed: (ImeAction) -> Unit ) = Unit override fun stopInput() = Unit override fun showSoftwareKeyboard() = Unit override fun hideSoftwareKeyboard() = Unit - override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) = Unit + override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) = Unit } \ No newline at end of file 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 0eee4e5466de1..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 @@ -21,6 +21,8 @@ import androidx.compose.ui.geometry.Rect 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.PlatformTextInputService +import androidx.compose.ui.text.input.TextFieldValue import org.w3c.dom.events.KeyboardEvent internal interface InputAwareInputService { @@ -29,19 +31,19 @@ internal interface InputAwareInputService { fun isVirtualKeyboard(): Boolean } -internal abstract class WebTextInputService : PlatformContextTextInputService, InputAwareInputService { +internal abstract class WebTextInputService : PlatformTextInputService, InputAwareInputService { private val webImeInputService = WebImeInputService(this) private val webKeyboardInputService = WebKeyboardInputService() - private fun delegatedService(): PlatformContextTextInputService { + private fun delegatedService(): PlatformTextInputService { return if (isVirtualKeyboard()) webImeInputService else webKeyboardInputService } override fun startInput( - value: TextFieldStateAdapter, + value: TextFieldValue, imeOptions: ImeOptions, onEditCommand: (List) -> Unit, - onImeActionPerformed: ((ImeAction) -> Unit)? + onImeActionPerformed: (ImeAction) -> Unit ) { delegatedService().startInput(value, imeOptions, onEditCommand, onImeActionPerformed) } @@ -58,7 +60,7 @@ internal abstract class WebTextInputService : PlatformContextTextInputService, I delegatedService().hideSoftwareKeyboard() } - override fun updateState(oldValue: TextFieldStateAdapter?, newValue: TextFieldStateAdapter) { + override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) { delegatedService().updateState(oldValue, newValue) } From d40d44e1e260cd7d02375e31a910689535dc08f8 Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Fri, 30 Aug 2024 01:26:15 +0200 Subject: [PATCH 10/11] Update API --- compose/ui/ui/api/desktop/ui.api | 34 ++++++-------------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/compose/ui/ui/api/desktop/ui.api b/compose/ui/ui/api/desktop/ui.api index 90c8b5af1f31d..9ef7cd6af7108 100644 --- a/compose/ui/ui/api/desktop/ui.api +++ b/compose/ui/ui/api/desktop/ui.api @@ -134,10 +134,10 @@ public final class androidx/compose/ui/ComposableSingletons$ImageComposeScene_sk public final class androidx/compose/ui/ComposeScene { public static final field $stable I - public fun (Landroidx/compose/ui/platform/PlatformContextTextInputService;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;Lkotlin/jvm/functions/Function0;)V - public synthetic fun (Landroidx/compose/ui/platform/PlatformContextTextInputService;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Landroidx/compose/ui/platform/PlatformContextTextInputService;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Lkotlin/jvm/functions/Function0;)V - public synthetic fun (Landroidx/compose/ui/platform/PlatformContextTextInputService;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Landroidx/compose/ui/text/input/PlatformTextInputService;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;Lkotlin/jvm/functions/Function0;)V + public synthetic fun (Landroidx/compose/ui/text/input/PlatformTextInputService;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Landroidx/compose/ui/text/input/PlatformTextInputService;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Lkotlin/jvm/functions/Function0;)V + public synthetic fun (Landroidx/compose/ui/text/input/PlatformTextInputService;Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;Lkotlin/jvm/functions/Function0;)V public synthetic fun (Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Landroidx/compose/ui/unit/LayoutDirection;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (Lkotlin/coroutines/CoroutineContext;Landroidx/compose/ui/unit/Density;Lkotlin/jvm/functions/Function0;)V @@ -3292,7 +3292,7 @@ public abstract interface class androidx/compose/ui/platform/PlatformContext { public fun getParentFocusManager ()Landroidx/compose/ui/focus/FocusManager; public fun getRootForTestListener ()Landroidx/compose/ui/platform/PlatformContext$RootForTestListener; public fun getSemanticsOwnerListener ()Landroidx/compose/ui/platform/PlatformContext$SemanticsOwnerListener; - public fun getTextInputService ()Landroidx/compose/ui/platform/PlatformContextTextInputService; + public fun getTextInputService ()Landroidx/compose/ui/text/input/PlatformTextInputService; public fun getTextToolbar ()Landroidx/compose/ui/platform/TextToolbar; public fun getViewConfiguration ()Landroidx/compose/ui/platform/ViewConfiguration; public abstract fun getWindowInfo ()Landroidx/compose/ui/platform/WindowInfo; @@ -3319,17 +3319,6 @@ public abstract interface class androidx/compose/ui/platform/PlatformContext$Sem public abstract fun onSemanticsOwnerRemoved (Landroidx/compose/ui/semantics/SemanticsOwner;)V } -public abstract interface class androidx/compose/ui/platform/PlatformContextTextInputService { - public abstract fun hideSoftwareKeyboard ()V - public fun notifyFocusedRect (Landroidx/compose/ui/geometry/Rect;)V - public abstract fun showSoftwareKeyboard ()V - public fun startInput ()V - public abstract fun startInput (Landroidx/compose/ui/platform/TextFieldStateAdapter;Landroidx/compose/ui/text/input/ImeOptions;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V - public abstract fun stopInput ()V - public abstract fun updateState (Landroidx/compose/ui/platform/TextFieldStateAdapter;Landroidx/compose/ui/platform/TextFieldStateAdapter;)V - public fun updateTextLayoutResult (Landroidx/compose/ui/platform/TextFieldStateAdapter;Landroidx/compose/ui/text/input/OffsetMapping;Landroidx/compose/ui/text/TextLayoutResult;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/geometry/Rect;Landroidx/compose/ui/geometry/Rect;)V -} - public abstract interface class androidx/compose/ui/platform/PlatformDragAndDropManager { public abstract fun drag-12SF9DM (Landroidx/compose/ui/draganddrop/DragAndDropTransferData;JLkotlin/jvm/functions/Function1;)Z public abstract fun getModifier ()Landroidx/compose/ui/Modifier; @@ -3383,12 +3372,7 @@ public abstract interface class androidx/compose/ui/platform/PlatformTextInputMe 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/platform/TextFieldStateAdapter; -} - -public final class androidx/compose/ui/platform/PlatformTextInputMethodRequest_skikoKt { - public static final fun asTextFieldStateAdapter (Landroidx/compose/ui/text/input/TextFieldValue;)Landroidx/compose/ui/platform/TextFieldStateAdapter; - public static final fun toTextFieldValue (Landroidx/compose/ui/platform/TextFieldStateAdapter;)Landroidx/compose/ui/text/input/TextFieldValue; + public abstract fun getState ()Landroidx/compose/ui/text/input/TextFieldValue; } public abstract interface class androidx/compose/ui/platform/PlatformTextInputModifierNode : androidx/compose/ui/node/DelegatableNode { @@ -3415,12 +3399,6 @@ public final class androidx/compose/ui/platform/TestTagKt { public static final fun testTag (Landroidx/compose/ui/Modifier;Ljava/lang/String;)Landroidx/compose/ui/Modifier; } -public abstract interface class androidx/compose/ui/platform/TextFieldStateAdapter { - public abstract fun getComposition-MzsxiRA ()Landroidx/compose/ui/text/TextRange; - public abstract fun getSelection-d9O1mEE ()J - public abstract fun getText ()Ljava/lang/CharSequence; -} - public abstract interface class androidx/compose/ui/platform/TextToolbar { public abstract fun getStatus ()Landroidx/compose/ui/platform/TextToolbarStatus; public abstract fun hide ()V From eabf11b01917de0731dc156cbf473dcd562fc249 Mon Sep 17 00:00:00 2001 From: Igor Demin Date: Fri, 30 Aug 2024 01:29:36 +0200 Subject: [PATCH 11/11] Mark PlatformTextInputMethodRequest as Experimental --- .../foundation/text/input/internal/TextInputSession.skiko.kt | 2 ++ .../ui/platform/PlatformTextInputMethodRequest.skiko.kt | 5 +++++ 2 files changed, 7 insertions(+) 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 49bc295da843c..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 @@ -17,6 +17,7 @@ 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 @@ -80,6 +81,7 @@ internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSe ) } +@OptIn(ExperimentalComposeUiApi::class) private data class SkikoPlatformTextInputMethodRequest( override val state: TextFieldValue, override val imeOptions: ImeOptions, 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 3ee3ac5dfb093..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,14 +16,19 @@ package androidx.compose.ui.platform +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)? }