Skip to content

Refactor Legacy Text Input to use PlatformTextInputSession #1974

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions compose/foundation/foundation/api/foundation.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -1740,6 +1740,7 @@ final val androidx.compose.foundation.text.input.internal/androidx_compose_found
final val androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_PartialGapBuffer$stableprop // androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_PartialGapBuffer$stableprop|#static{}androidx_compose_foundation_text_input_internal_PartialGapBuffer$stableprop[0]
final val androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_SelectionWedgeAffinity$stableprop // androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_SelectionWedgeAffinity$stableprop|#static{}androidx_compose_foundation_text_input_internal_SelectionWedgeAffinity$stableprop[0]
final val androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_SingleLineCodepointTransformation$stableprop // androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_SingleLineCodepointTransformation$stableprop|#static{}androidx_compose_foundation_text_input_internal_SingleLineCodepointTransformation$stableprop[0]
final val androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_SkikoPlatformTextInputMethodRequest$stableprop // androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_SkikoPlatformTextInputMethodRequest$stableprop|#static{}androidx_compose_foundation_text_input_internal_SkikoPlatformTextInputMethodRequest$stableprop[0]
final val androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_TextFieldCoreModifier$stableprop // androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_TextFieldCoreModifier$stableprop|#static{}androidx_compose_foundation_text_input_internal_TextFieldCoreModifier$stableprop[0]
final val androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_TextFieldCoreModifierNode$stableprop // androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_TextFieldCoreModifierNode$stableprop|#static{}androidx_compose_foundation_text_input_internal_TextFieldCoreModifierNode$stableprop[0]
final val androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_TextFieldDecoratorModifier$stableprop // androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_TextFieldDecoratorModifier$stableprop|#static{}androidx_compose_foundation_text_input_internal_TextFieldDecoratorModifier$stableprop[0]
Expand Down Expand Up @@ -2188,6 +2189,7 @@ final fun androidx.compose.foundation.text.input.internal/androidx_compose_found
final fun androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_PartialGapBuffer$stableprop_getter(): kotlin/Int // androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_PartialGapBuffer$stableprop_getter|androidx_compose_foundation_text_input_internal_PartialGapBuffer$stableprop_getter(){}[0]
final fun androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_SelectionWedgeAffinity$stableprop_getter(): kotlin/Int // androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_SelectionWedgeAffinity$stableprop_getter|androidx_compose_foundation_text_input_internal_SelectionWedgeAffinity$stableprop_getter(){}[0]
final fun androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_SingleLineCodepointTransformation$stableprop_getter(): kotlin/Int // androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_SingleLineCodepointTransformation$stableprop_getter|androidx_compose_foundation_text_input_internal_SingleLineCodepointTransformation$stableprop_getter(){}[0]
final fun androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_SkikoPlatformTextInputMethodRequest$stableprop_getter(): kotlin/Int // androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_SkikoPlatformTextInputMethodRequest$stableprop_getter|androidx_compose_foundation_text_input_internal_SkikoPlatformTextInputMethodRequest$stableprop_getter(){}[0]
final fun androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_TextFieldCoreModifier$stableprop_getter(): kotlin/Int // androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_TextFieldCoreModifier$stableprop_getter|androidx_compose_foundation_text_input_internal_TextFieldCoreModifier$stableprop_getter(){}[0]
final fun androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_TextFieldCoreModifierNode$stableprop_getter(): kotlin/Int // androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_TextFieldCoreModifierNode$stableprop_getter|androidx_compose_foundation_text_input_internal_TextFieldCoreModifierNode$stableprop_getter(){}[0]
final fun androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_TextFieldDecoratorModifier$stableprop_getter(): kotlin/Int // androidx.compose.foundation.text.input.internal/androidx_compose_foundation_text_input_internal_TextFieldDecoratorModifier$stableprop_getter|androidx_compose_foundation_text_input_internal_TextFieldDecoratorModifier$stableprop_getter(){}[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,15 +213,15 @@ internal class TextFieldDelegate {
textInputSession.notifyFocusedRect(
focusedRectInRoot(
layoutResult = textLayoutResult,
layoutCoordinates = layoutCoordinates,
focusOffset = offsetMapping.originalToTransformed(value.selection.max),
sizeForDefaultText = {
computeSizeForDefaultText(
textDelegate.style,
textDelegate.density,
textDelegate.fontFamilyResolver
)
}
},
convertLocalToRoot = layoutCoordinates::localToRoot
)
)
}
Expand Down Expand Up @@ -432,8 +432,8 @@ internal class TextFieldDelegate {
/** Computes the bounds of the area where text editing is in progress, relative to the root. */
internal fun focusedRectInRoot(
layoutResult: TextLayoutResult,
layoutCoordinates: LayoutCoordinates,
focusOffset: Int,
convertLocalToRoot: (Offset) -> Offset,
sizeForDefaultText: () -> IntSize
): Rect {
val bbox =
Expand All @@ -449,6 +449,6 @@ internal fun focusedRectInRoot(
Rect(0f, 0f, 1.0f, size.height.toFloat())
}
}
val globalLT = layoutCoordinates.localToRoot(Offset(bbox.left, bbox.top))
val globalLT = convertLocalToRoot(Offset(bbox.left, bbox.top))
return Rect(Offset(globalLT.x, globalLT.y), Size(bbox.width, bbox.height))
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,49 +16,76 @@

package androidx.compose.foundation.text.input.internal

import androidx.compose.foundation.text.computeSizeForDefaultText
import androidx.compose.foundation.text.focusedRectInRoot
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Matrix
import androidx.compose.ui.platform.LocalTextInputService
import androidx.compose.ui.text.InternalTextApi
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.CommitTextCommand
import androidx.compose.ui.text.input.DeleteSurroundingTextCommand
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.SetComposingTextCommand
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.text.input.TextInputService
import androidx.compose.ui.text.input.TextInputSession
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.filterNotNull

// TODO remove after https://youtrack.jetbrains.com/issue/COMPOSE-740/Implement-BasicTextField2
@Suppress("DEPRECATION")
@OptIn(ExperimentalComposeUiApi::class)
@Composable
internal actual fun legacyTextInputServiceAdapterAndService():
Pair<LegacyPlatformTextInputServiceAdapter, TextInputService>
{
val service = LocalTextInputService.current!!
val adapter = remember(service) {
object : LegacyPlatformTextInputServiceAdapter() {
private var session: TextInputSession? = null
override fun startStylusHandwriting() {}
Pair<LegacyPlatformTextInputServiceAdapter, TextInputService> {
return remember {
val adapter = object : LegacyPlatformTextInputServiceAdapter() {
private var job: Job? = null

private var textFieldValue by mutableStateOf(TextFieldValue())
private var textLayoutResult by mutableStateOf<TextLayoutResult?>(null)
private var focusedRectInRoot by mutableStateOf(Rect.Zero)
private var textFieldRectInRoot by mutableStateOf(Rect.Zero)
private var textClippingRectInRoot by mutableStateOf(Rect.Zero)

override fun startInput(
value: TextFieldValue,
imeOptions: ImeOptions,
onEditCommand: (List<EditCommand>) -> Unit,
onImeActionPerformed: (ImeAction) -> Unit
) {
session = service.startInput(value, imeOptions, onEditCommand, onImeActionPerformed)
textFieldValue = value
val node = textInputModifierNode ?: return

job = node.launchTextInputSession {
startInputMethod(
makeRequest(
imeOptions = imeOptions,
onEditCommand = onEditCommand,
onImeActionPerformed = onImeActionPerformed
)
)
}
}

override fun stopInput() {
session?.dispose()
session = null
job?.cancel()
job = null
textFieldValue = TextFieldValue()
}

override fun updateState(oldValue: TextFieldValue?, newValue: TextFieldValue) {
session?.updateState(oldValue, newValue)
this.textFieldValue = newValue
}

override fun updateTextLayoutResult(
Expand All @@ -69,16 +96,90 @@ internal actual fun legacyTextInputServiceAdapterAndService():
innerTextFieldBounds: Rect,
decorationBoxBounds: Rect
) {
session?.updateTextLayoutResult(
textFieldValue,
offsetMapping,
textLayoutResult,
textFieldToRootTransform,
innerTextFieldBounds,
decorationBoxBounds
this.textFieldValue = textFieldValue
this.textLayoutResult = textLayoutResult

val matrix = Matrix().also { textFieldToRootTransform(it) }
textFieldRectInRoot = matrix.map(decorationBoxBounds)
textClippingRectInRoot = matrix.map(innerTextFieldBounds)
focusedRectInRoot = focusedRectInRoot(
layoutResult = textLayoutResult,
focusOffset = textFieldValue.selection.max,
sizeForDefaultText = {
textLayoutResult.layoutInput.let {
computeSizeForDefaultText(it.style, it.density, it.fontFamilyResolver)
}
},
convertLocalToRoot = matrix::map
)
}

override fun startStylusHandwriting() {}

private fun makeRequest(
imeOptions: ImeOptions,
onEditCommand: (List<EditCommand>) -> Unit,
onImeActionPerformed: (ImeAction) -> Unit
): SkikoPlatformTextInputMethodRequest {
val textEditorState = object : TextEditorState {
override val selection: TextRange get() = textFieldValue.selection
override val composition: TextRange? get() = textFieldValue.composition
override val length: Int get() = textFieldValue.text.length
override fun get(index: Int): Char = textFieldValue.text[index]
override fun subSequence(startIndex: Int, endIndex: Int): CharSequence =
textFieldValue.text.subSequence(startIndex, endIndex)
}

val editBlock: (block: TextEditingScope.() -> Unit) -> Unit = { block ->
object : TextEditingScope {
fun runOnEditCommand(command: EditCommand) {
onEditCommand(listOf(command))
}

override fun deleteSurroundingTextInCodePoints(
lengthBeforeCursor: Int,
lengthAfterCursor: Int
) {
runOnEditCommand(
DeleteSurroundingTextCommand(lengthBeforeCursor, lengthAfterCursor)
)
}

override fun commitText(
text: CharSequence,
newCursorPosition: Int
) {
runOnEditCommand(
CommitTextCommand(text.toString(), newCursorPosition)
)
}

override fun setComposingText(
text: CharSequence,
newCursorPosition: Int
) {
runOnEditCommand(
SetComposingTextCommand(text.toString(), newCursorPosition)
)
}
}.block()
}

return SkikoPlatformTextInputMethodRequest(
value = { textFieldValue },
state = textEditorState,
imeOptions = imeOptions,
onEditCommand = onEditCommand,
onImeAction = onImeActionPerformed,
outputValue = snapshotFlow { textFieldValue },
textLayoutResult = snapshotFlow { textLayoutResult }.filterNotNull(),
focusedRectInRoot = snapshotFlow { focusedRectInRoot },
textFieldRectInRoot = snapshotFlow { textFieldRectInRoot },
textClippingRectInRoot = snapshotFlow { textClippingRectInRoot },
editText = editBlock
)
}
}
adapter to TextInputService(adapter)
}
return adapter to service
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,13 @@ internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSe
val layoutCoords = layoutState.textLayoutNodeCoordinates ?: return@snapshotFlow null
focusedRectInRoot(
layoutResult = layoutResult,
layoutCoordinates = layoutCoords,
focusOffset = state.visualText.selection.max,
sizeForDefaultText = {
layoutResult.layoutInput.let {
computeSizeForDefaultText(it.style, it.density, it.fontFamilyResolver)
}
}
},
convertLocalToRoot = layoutCoords::localToRoot,
)
}.filterNotNull()

Expand All @@ -130,7 +130,6 @@ internal actual suspend fun PlatformTextInputSession.platformSpecificTextInputSe
imeOptions = imeOptions,
onEditCommand = ::onEditCommand,
onImeAction = onImeAction,
editProcessor = editProcessor,
outputValue = outputValueFlow,
textLayoutResult = snapshotFlow(layoutState::layoutResult).filterNotNull(),
focusedRectInRoot = focusedRectInRootFlow,
Expand Down Expand Up @@ -246,13 +245,12 @@ private fun TextEditingScope(buffer: TextFieldBuffer) = object : TextEditingScop


@OptIn(ExperimentalComposeUiApi::class)
private data class SkikoPlatformTextInputMethodRequest(
internal data class SkikoPlatformTextInputMethodRequest(
override val value: () -> TextFieldValue,
override val state: TextEditorState,
override val imeOptions: ImeOptions,
override val onEditCommand: (List<EditCommand>) -> Unit,
override val onImeAction: ((ImeAction) -> Unit)?,
override val editProcessor: EditProcessor?,
override val outputValue: Flow<TextFieldValue>,
override val textLayoutResult: Flow<TextLayoutResult>,
override val focusedRectInRoot: Flow<Rect>,
Expand Down
Loading