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 ff929419988b5..a99ed005672d6 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 @@ -18,7 +18,9 @@ package androidx.compose.foundation.text.input.internal import androidx.compose.foundation.content.internal.ReceiveContentConfiguration import androidx.compose.foundation.text.computeSizeForDefaultText +import androidx.compose.foundation.text.input.TextFieldBuffer import androidx.compose.foundation.text.input.TextFieldCharSequence +import androidx.compose.foundation.text.input.delete import androidx.compose.foundation.text.input.setSelectionCoerced import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.ExperimentalComposeUiApi @@ -30,19 +32,23 @@ import androidx.compose.ui.platform.PlatformTextInputMethodRequest import androidx.compose.ui.platform.PlatformTextInputSession import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.text.TextLayoutResult +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 androidx.compose.ui.text.input.TextEditingScope +import androidx.compose.ui.text.input.TextEditorState import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.IntSize +import kotlin.math.absoluteValue +import kotlin.math.sign import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.launch -@OptIn(ExperimentalComposeUiApi::class) internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSession( state: TransformedTextFieldState, layoutState: TextLayoutState, @@ -77,10 +83,19 @@ internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSe } } + fun editText(block: TextEditingScope.() -> Unit) { + state.editUntransformedTextAsUser { + with(TextEditingScope(this)) { + block() + } + } + } + coroutineScope { - launch { - state.collectImeNotifications { _, newValue, _ -> - updateTextFieldValue(newValue.toTextFieldValue()) + val outputValueFlow = callbackFlow { + state.collectImeNotifications { oldValue, newValue, restartIme -> + println("IME Notification: oldValue=$oldValue, newValue=$newValue, restartIme=$restartIme") + trySend(newValue.toTextFieldValue()) } } @@ -101,13 +116,16 @@ internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSe startInputMethod( SkikoPlatformTextInputMethodRequest( - state = state.untransformedText.toTextFieldValue(), + value = { state.untransformedText.toTextFieldValue() }, + state = state.untransformedText.asTextEditorState(), imeOptions = imeOptions, onEditCommand = ::onEditCommand, onImeAction = onImeAction, editProcessor = editProcessor, + outputValue = outputValueFlow, textLayoutResult = snapshotFlow(layoutState::layoutResult).filterNotNull(), focusedRectInRoot = focusedRectInRootFlow, + editText = ::editText ) ) } @@ -116,15 +134,154 @@ internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSe private fun TextFieldCharSequence.toTextFieldValue() = TextFieldValue(toString(), selection, composition) +private fun TextFieldCharSequence.asTextEditorState() = object : TextEditorState { + + override val length: Int + get() = this@asTextEditorState.length + + override fun get(index: Int): Char = this@asTextEditorState[index] + + override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { + return this@asTextEditorState.subSequence(startIndex, endIndex) + } + + override val selection: TextRange + get() = this@asTextEditorState.selection + + override val composition: TextRange? + get() = this@asTextEditorState.composition + +} + +/** + * Helper function that returns true when [high] is a Unicode high-surrogate code unit and [low] is + * a Unicode low-surrogate code unit. + */ +private fun isSurrogatePair(high: Char, low: Char): Boolean = + high.isHighSurrogate() && low.isLowSurrogate() + + +/** + * Returns the index, in characters, of the code point at distance [offset] from [index]. + * + * If there aren't enough codepoints in the correct direction, returns 0 (if [offset] is negative) + * or the length of the char sequence (if [offset] is positive). + */ +private fun CharSequence.indexOfCodePointAtOffset(index: Int, offset: Int): Int { + val sign = offset.sign + val distance = offset.absoluteValue + + var currentOffset = index + for (i in 0 until distance) { + currentOffset += sign + if (currentOffset <= 0) { + return 0 + } else if (currentOffset >= length) { + return length + } + + + val lead = this[currentOffset - 1] + val trail = this[currentOffset] + + if (isSurrogatePair(lead, trail)) { + currentOffset += sign + } + } + + return currentOffset +} + + +private var TextFieldBuffer.cursor: Int + get() = if (selection.collapsed) selection.end else -1 + set(value) { + setSelectionCoerced(value, value) + } + +private fun TextEditingScope(buffer: TextFieldBuffer) = object : TextEditingScope { + + override fun deleteSurroundingTextInCodePoints( + lengthBeforeCursor: Int, + lengthAfterCursor: Int + ) { + val charSequence = buffer.asCharSequence() + val selection = buffer.selection + buffer.delete( + start = selection.end, + end = charSequence.indexOfCodePointAtOffset( + index = selection.end, + offset = lengthAfterCursor + ) + ) + buffer.delete( + start = charSequence.indexOfCodePointAtOffset( + index = selection.start, + offset = -lengthBeforeCursor + ), + end = selection.start + ) + } + + override fun commitText(text: CharSequence, newCursorPosition: Int) { + // API description says replace ongoing composition text if there. Then, if there is no + // composition text, insert text into cursor position or replace selection. + val replacementRange = buffer.composition ?: buffer.selection + buffer.replace(replacementRange.start, replacementRange.end, text) + + // After replace function is called, the editing buffer places the cursor at the end of the + // modified range. + val newCursor = buffer.cursor + + // See API description for the meaning of newCursorPosition. + val newCursorInBuffer = + if (newCursorPosition > 0) { + newCursor + newCursorPosition - 1 + } else { + newCursor + newCursorPosition - text.length + } + buffer.setSelectionCoerced(newCursorInBuffer, newCursorInBuffer) + } + + override fun setComposingText(text: CharSequence, newCursorPosition: Int) { + val replacementRange = buffer.composition ?: buffer.selection + // API doc says, if there is ongoing composing text, replace it with new text. + // If there is no composing text, insert composing text into cursor position with + // removing selected text if any. + buffer.replace(replacementRange.start, replacementRange.end, text) + if (text.isNotEmpty()) { + buffer.setComposition(replacementRange.start, replacementRange.start + text.length) + } + + // After replace function is called, the editing buffer places the cursor at the end of the + // modified range. + val newCursor = buffer.cursor + + // See API description for the meaning of newCursorPosition. + val newCursorInBuffer = + if (newCursorPosition > 0) { + newCursor + newCursorPosition - 1 + } else { + newCursor + newCursorPosition - text.length + } + + buffer.cursor = newCursorInBuffer + } +} + + @OptIn(ExperimentalComposeUiApi::class) private data class SkikoPlatformTextInputMethodRequest( - override val state: TextFieldValue, + override val value: () -> TextFieldValue, + override val state: TextEditorState, override val imeOptions: ImeOptions, override val onEditCommand: (List) -> Unit, override val onImeAction: ((ImeAction) -> Unit)?, override val editProcessor: EditProcessor?, + override val outputValue: Flow, override val textLayoutResult: Flow, - override val focusedRectInRoot: Flow + override val focusedRectInRoot: Flow, + override val editText: (block: TextEditingScope.() -> Unit) -> Unit ): PlatformTextInputMethodRequest /** diff --git a/compose/ui/ui-text/api/desktop/ui-text.api b/compose/ui/ui-text/api/desktop/ui-text.api index 8c8a21c42224e..8f71bc9fcd778 100644 --- a/compose/ui/ui-text/api/desktop/ui-text.api +++ b/compose/ui/ui-text/api/desktop/ui-text.api @@ -1359,6 +1359,12 @@ public abstract interface class androidx/compose/ui/text/input/PlatformTextInput public fun updateTextLayoutResult (Landroidx/compose/ui/text/input/TextFieldValue;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/text/input/PlatformTextInputService2 { + public abstract fun focusedRectChanged (Landroidx/compose/ui/geometry/Rect;)V + public abstract fun startInput (Landroidx/compose/ui/text/input/TextEditorState;Landroidx/compose/ui/text/input/ImeOptions;Lkotlin/jvm/functions/Function1;)V + public abstract fun stopInput ()V +} + public final class androidx/compose/ui/text/input/SetComposingRegionCommand : androidx/compose/ui/text/input/EditCommand { public static final field $stable I public fun (II)V @@ -1394,6 +1400,17 @@ public final class androidx/compose/ui/text/input/SetSelectionCommand : androidx public fun toString ()Ljava/lang/String; } +public abstract interface class androidx/compose/ui/text/input/TextEditingScope { + public abstract fun commitText (Ljava/lang/CharSequence;I)V + public abstract fun deleteSurroundingTextInCodePoints (II)V + public abstract fun setComposingText (Ljava/lang/CharSequence;I)V +} + +public abstract interface class androidx/compose/ui/text/input/TextEditorState : java/lang/CharSequence { + public abstract fun getComposition-MzsxiRA ()Landroidx/compose/ui/text/TextRange; + public abstract fun getSelection-d9O1mEE ()J +} + public final class androidx/compose/ui/text/input/TextFieldValue { public static final field $stable I public static final field Companion Landroidx/compose/ui/text/input/TextFieldValue$Companion; diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/input/PlatformTextInputService2.kt b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/input/PlatformTextInputService2.kt new file mode 100644 index 0000000000000..e81b5f83ee383 --- /dev/null +++ b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/input/PlatformTextInputService2.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2025 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.text.input + +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.text.TextRange + +interface TextEditorState : CharSequence { + + val selection: TextRange + val composition: TextRange? + +} + +interface TextEditingScope { + /** + * Deletes text around the cursor. + * + * This intends to replicate [DeleteSurroundingTextInCodePointsCommand] for + * [PlatformTextInputService2]. + */ + fun deleteSurroundingTextInCodePoints(lengthBeforeCursor: Int, lengthAfterCursor: Int) + + /** + * Commits text and repositions the cursor. + * + * This intends to replicate [CommitTextCommand] for [PlatformTextInputService2]. + */ + fun commitText(text: CharSequence, newCursorPosition: Int) + + /** + * Sets the composing text and repositions the cursor. + * + * This intends to replicate [SetComposingTextCommand] for [PlatformTextInputService2]. + */ + fun setComposingText(text: CharSequence, newCursorPosition: Int) +} + +interface PlatformTextInputService2 { + + fun startInput( + state: TextEditorState, + imeOptions: ImeOptions, + editText: (block: TextEditingScope.() -> Unit) -> Unit, + ) + + fun stopInput() + + fun focusedRectChanged(rect: Rect) + +} \ No newline at end of file diff --git a/compose/ui/ui/api/desktop/ui.api b/compose/ui/ui/api/desktop/ui.api index c7b10a81f7d2b..34e9e5427ca21 100644 --- a/compose/ui/ui/api/desktop/ui.api +++ b/compose/ui/ui/api/desktop/ui.api @@ -3584,12 +3584,15 @@ public abstract interface class androidx/compose/ui/platform/PlatformTextInputIn public abstract interface class androidx/compose/ui/platform/PlatformTextInputMethodRequest { public abstract fun getEditProcessor ()Landroidx/compose/ui/text/input/EditProcessor; + public abstract fun getEditText ()Lkotlin/jvm/functions/Function1; public abstract fun getFocusedRectInRoot ()Lkotlinx/coroutines/flow/Flow; public abstract fun getImeOptions ()Landroidx/compose/ui/text/input/ImeOptions; public abstract fun getOnEditCommand ()Lkotlin/jvm/functions/Function1; public abstract fun getOnImeAction ()Lkotlin/jvm/functions/Function1; - public abstract fun getState ()Landroidx/compose/ui/text/input/TextFieldValue; + public abstract fun getOutputValue ()Lkotlinx/coroutines/flow/Flow; + public abstract fun getState ()Landroidx/compose/ui/text/input/TextEditorState; public abstract fun getTextLayoutResult ()Lkotlinx/coroutines/flow/Flow; + public abstract fun getValue ()Lkotlin/jvm/functions/Function0; } public abstract interface class androidx/compose/ui/platform/PlatformTextInputModifierNode : androidx/compose/ui/node/DelegatableNode { @@ -3602,7 +3605,6 @@ public final class androidx/compose/ui/platform/PlatformTextInputModifierNodeKt public abstract interface class androidx/compose/ui/platform/PlatformTextInputSession { public abstract fun startInputMethod (Landroidx/compose/ui/platform/PlatformTextInputMethodRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; - public fun updateTextFieldValue (Landroidx/compose/ui/text/input/TextFieldValue;)V } public abstract interface class androidx/compose/ui/platform/PlatformTextInputSessionScope : androidx/compose/ui/platform/PlatformTextInputSession, kotlinx/coroutines/CoroutineScope { 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 fcb04b9db58c0..fc5abc3362c3a 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 @@ -104,7 +104,7 @@ internal class DesktopTextInputService(private val component: PlatformComponent) } fun inputMethodTextChanged(event: InputMethodEvent) { - if (!event.isConsumed) { + if ((currentInput != null) && !event.isConsumed) { replaceInputMethodText(event) event.consume() } diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopTextInputService2.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopTextInputService2.kt new file mode 100644 index 0000000000000..1a917b73bd127 --- /dev/null +++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopTextInputService2.kt @@ -0,0 +1,226 @@ +/* + * Copyright 2025 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.text.TextRange +import androidx.compose.ui.text.input.CommitTextCommand +import androidx.compose.ui.text.input.DeleteSurroundingTextInCodePointsCommand +import androidx.compose.ui.text.input.EditCommand +import androidx.compose.ui.text.input.ImeOptions +import androidx.compose.ui.text.input.PlatformTextInputService2 +import androidx.compose.ui.text.input.SetComposingTextCommand +import androidx.compose.ui.text.input.TextEditingScope +import androidx.compose.ui.text.input.TextEditorState +import androidx.compose.ui.text.substring +import java.awt.Rectangle +import java.awt.event.InputMethodEvent +import java.awt.event.KeyEvent +import java.awt.font.TextHitInfo +import java.awt.im.InputMethodRequests +import java.text.AttributedCharacterIterator +import java.text.AttributedString +import java.text.CharacterIterator +import kotlin.math.max +import kotlin.math.min +import org.jetbrains.skiko.OS +import org.jetbrains.skiko.hostOs + +internal class DesktopTextInputService2( + private val component: PlatformComponent +) : PlatformTextInputService2 { + + private var currentInputMethodRequests: InputMethodRequestsImpl? = null + + override fun startInput( + state: TextEditorState, + imeOptions: ImeOptions, + editText: (block: TextEditingScope.() -> Unit) -> Unit + ) { + component.enableInput( + InputMethodRequestsImpl(component, state, editText).also { + currentInputMethodRequests = it + } + ) + } + + override fun stopInput() { + component.disableInput() + + this.currentInputMethodRequests = null + } + + override fun focusedRectChanged(rect: Rect) { + currentInputMethodRequests?.focusedRect = rect + } + + fun onKeyEvent(keyEvent: KeyEvent) { + when (keyEvent.id) { + KeyEvent.KEY_TYPED -> + currentInputMethodRequests?.charKeyPressed = true + KeyEvent.KEY_RELEASED -> + currentInputMethodRequests?.charKeyPressed = false + } + } + + fun inputMethodTextChanged(event: InputMethodEvent) { + val inputMethodRequests = currentInputMethodRequests ?: return + if (!event.isConsumed) { + inputMethodRequests.replaceInputMethodText(event) + event.consume() + } + } + +} + +private class InputMethodRequestsImpl( + private val component: PlatformComponent, + private val state: TextEditorState, + private val editText: (block: TextEditingScope.() -> Unit) -> Unit +) : InputMethodRequests { + + private val selection: TextRange + get() = state.selection + + private val composition: TextRange? + get() = state.composition + + var focusedRect: Rect? = null + + // This is required to support input of accented characters using press-and-hold method (http://support.apple.com/kb/PH11264). + // JDK currently properly supports this functionality only for TextComponent/JTextComponent descendants. + // For our editor component we need this workaround. + // After https://bugs.openjdk.java.net/browse/JDK-8074882 is fixed, this workaround should be replaced with a proper solution. + var charKeyPressed: Boolean = false + var needToDeletePreviousChar: Boolean = false + + override fun getLocationOffset(x: Int, y: Int): TextHitInfo? { + if (composition != null) { + // TODO: to properly implement this method we need to somehow have access to + // Paragraph at this point + return TextHitInfo.leading(0) + } + return null + } + + override fun cancelLatestCommittedText( + attributes: Array? + ): AttributedCharacterIterator? { + return null + } + + override fun getInsertPositionOffset(): Int { + val composedStartIndex = composition?.start ?: 0 + val composedEndIndex = composition?.end ?: 0 + + val caretIndex = selection.start + if (caretIndex < composedStartIndex) { + return caretIndex + } + if (caretIndex < composedEndIndex) { + return composedStartIndex + } + return caretIndex - (composedEndIndex - composedStartIndex) + } + + override fun getCommittedTextLength() = + state.length - (composition?.length ?: 0) + + override fun getSelectedText( + attributes: Array? + ): AttributedCharacterIterator { + if (charKeyPressed && (hostOs == OS.MacOS)) { + needToDeletePreviousChar = true + } + val str = state.substring(selection) + return AttributedString(str).iterator + } + + override fun getTextLocation(offset: TextHitInfo?): Rectangle? { + return focusedRect?.let { + val x = (it.right / component.density.density).toInt() + + component.locationOnScreen.x + val y = (it.top / component.density.density).toInt() + + component.locationOnScreen.y + Rectangle(x, y, it.width.toInt(), it.height.toInt()) + } + } + + override fun getCommittedText( + beginIndex: Int, + endIndex: Int, + attributes: Array? + ): AttributedCharacterIterator { + val comp = composition + // When input is performed with Pinyin and backspace pressed, + // comp is null and beginIndex > endIndex. + // TODO Check is this an expected behavior? + val range = TextRange( + start = beginIndex.coerceAtMost(state.length), + end = endIndex.coerceAtMost(state.length) + ) + if (comp == null) { + val res = state.substring(range) + return AttributedString(res).iterator + } + val committed = state.substring( + TextRange( + min(range.min, comp.min).coerceAtMost(state.length), + max(range.max, comp.max).coerceAtMost(state.length) + ) + ) + return AttributedString(committed).iterator + } + + fun replaceInputMethodText(event: InputMethodEvent) { + val committed = event.text?.toStringUntil(event.committedCharacterCount) ?: "" + val composing = event.text?.toStringFrom(event.committedCharacterCount) ?: "" + + editText { + if (needToDeletePreviousChar && selection.min > 0 && composing.isEmpty()) { + needToDeletePreviousChar = false + deleteSurroundingTextInCodePoints(1, 0) + } + commitText(committed, 1) + if (composing.isNotEmpty()) { + setComposingText(composing, 1) + } + } + } + +} + + +private fun AttributedCharacterIterator.toStringUntil(index: Int) = StringBuilder().apply { + var i = index + if (i > 0) { + var c: Char = setIndex(0) + while (i > 0) { + append(c) + c = next() + i-- + } + } +} + +private fun AttributedCharacterIterator.toStringFrom(index: Int) = StringBuilder().apply { + var c: Char = setIndex(index) + while (c != CharacterIterator.DONE) { + append(c) + c = next() + } +} 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 d68aeee9e42fb..667a59a7c8dbd 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,7 +20,6 @@ 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.ExperimentalComposeUiApi import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.SessionMutex import androidx.compose.ui.awt.AwtEventListener @@ -46,6 +45,7 @@ import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.platform.AwtDragAndDropManager import androidx.compose.ui.platform.DelegateRootForTestListener import androidx.compose.ui.platform.DesktopTextInputService +import androidx.compose.ui.platform.DesktopTextInputService2 import androidx.compose.ui.platform.EmptyViewConfiguration import androidx.compose.ui.platform.PlatformComponent import androidx.compose.ui.platform.PlatformContext @@ -59,7 +59,6 @@ import androidx.compose.ui.platform.a11y.AccessibilityController import androidx.compose.ui.platform.a11y.ComposeSceneAccessible import androidx.compose.ui.scene.skia.SkiaLayerComponent import androidx.compose.ui.semantics.SemanticsOwner -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection @@ -142,6 +141,7 @@ internal class ComposeSceneMediator( private val platformComponent = DesktopPlatformComponent() private val textInputService = DesktopTextInputService(platformComponent) + private val textInputService2 = DesktopTextInputService2(platformComponent) private val _platformContext = DesktopPlatformContext() val platformContext: PlatformContext get() = _platformContext @@ -229,6 +229,9 @@ internal class ComposeSceneMediator( catchExceptions { textInputService.inputMethodTextChanged(event) } + catchExceptions { + textInputService2.inputMethodTextChanged(event) + } } } private val focusListener = object : FocusListener { @@ -469,6 +472,7 @@ internal class ComposeSceneMediator( } val composeEvent = event.toComposeEvent() textInputService.onKeyEvent(event) + textInputService2.onKeyEvent(event) windowContext.setKeyboardModifiers(composeEvent.internal.modifiers) if (onPreviewKeyEvent(composeEvent) || scene.sendKeyEvent(composeEvent) || @@ -777,29 +781,34 @@ internal class ComposeSceneMediator( coroutineScope { launch { request.focusedRectInRoot.collect { - textInputService.notifyFocusedRect(it) +// textInputService.notifyFocusedRect(it) + textInputService2.focusedRectChanged(it) } } suspendCancellableCoroutine { continuation -> - textInputService.startInput( - value = request.state, +// textInputService.startInput( +// value = request.value(), +// imeOptions = request.imeOptions, +// onEditCommand = request.onEditCommand, +// onImeActionPerformed = request.onImeAction ?: {} +// ) +// +// continuation.invokeOnCancellation { +// textInputService.stopInput() +// } + textInputService2.startInput( + state = request.state, imeOptions = request.imeOptions, - onEditCommand = request.onEditCommand, - onImeActionPerformed = request.onImeAction ?: {} + editText = request.editText, ) continuation.invokeOnCancellation { - textInputService.stopInput() + textInputService2.stopInput() } } } } - - @ExperimentalComposeUiApi - override fun updateTextFieldValue(newValue: TextFieldValue) { - textInputService.updateState(oldValue = null, newValue = newValue) - } } 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 9b7bf190f03f3..5a9f494361960 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 @@ -17,19 +17,25 @@ package androidx.compose.ui.platform import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.text.TextLayoutResult 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 androidx.compose.ui.text.input.TextEditingScope +import androidx.compose.ui.text.input.TextEditorState import androidx.compose.ui.text.input.TextFieldValue import kotlinx.coroutines.flow.Flow actual interface PlatformTextInputMethodRequest { /** The editor state. */ @ExperimentalComposeUiApi - val state: TextFieldValue + val value: () -> TextFieldValue + + /** The editor state. */ + @ExperimentalComposeUiApi + val state: TextEditorState /** Keyboard configuration such as single line, autocorrect etc. */ @ExperimentalComposeUiApi @@ -47,6 +53,9 @@ actual interface PlatformTextInputMethodRequest { @ExperimentalComposeUiApi val editProcessor: EditProcessor? + @ExperimentalComposeUiApi + val outputValue: Flow + /** * A flow with the layout of text in the editor's. * @@ -62,4 +71,7 @@ actual interface PlatformTextInputMethodRequest { */ @ExperimentalComposeUiApi val focusedRectInRoot: Flow + + @ExperimentalComposeUiApi + val editText: (block: TextEditingScope.() -> Unit) -> Unit } diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformTextInputSession.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/PlatformTextInputSession.skiko.kt index d20794681f46a..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,12 +16,6 @@ package androidx.compose.ui.platform -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.text.input.TextFieldValue - actual interface PlatformTextInputSession { actual suspend fun startInputMethod(request: PlatformTextInputMethodRequest): Nothing - - @ExperimentalComposeUiApi - fun updateTextFieldValue(newValue: TextFieldValue) = Unit } diff --git a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt index 8c3be3d26ac9e..08bc80802cf4f 100644 --- a/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt +++ b/compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/scene/ComposeSceneMediator.uikit.kt @@ -58,7 +58,6 @@ import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.platform.WindowInfo import androidx.compose.ui.platform.lerp import androidx.compose.ui.semantics.SemanticsOwner -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.uikit.LocalKeyboardOverlapHeight import androidx.compose.ui.uikit.OnFocusBehavior import androidx.compose.ui.uikit.density @@ -616,6 +615,11 @@ internal class ComposeSceneMediator( sessionInitializer = { null } ) { coroutineScope { + launch { + request.outputValue.collect { + textInputService.updateState(oldValue = null, newValue = it) + } + } launch { request.textLayoutResult.collect { textInputService.updateTextLayoutResult(it) @@ -623,7 +627,7 @@ internal class ComposeSceneMediator( } suspendCancellableCoroutine { continuation -> textInputService.startInput( - value = request.state, + value = request.value(), imeOptions = request.imeOptions, editProcessor = request.editProcessor, onEditCommand = request.onEditCommand, @@ -636,10 +640,6 @@ internal class ComposeSceneMediator( } } } - - override fun updateTextFieldValue(newValue: TextFieldValue) { - textInputService.updateState(oldValue = null, newValue = newValue) - } } }