Skip to content
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

Batch frequent Marked Text edit commands #1692

Merged
merged 4 commits into from
Nov 29, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package androidx.compose.ui.platform

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.DpOffset

internal interface IOSSkikoInput {
Expand All @@ -27,6 +26,16 @@ internal interface IOSSkikoInput {

fun endFloatingCursor()

/**
* Delays all edit commands until [endEditBatch] is being called.
*/
fun beginEditBatch()

/**
* Performs all editing commands, starting from the [beginEditBatch] call.
*/
fun endEditBatch()

/**
* A Boolean value that indicates whether the text-entry object has any text.
* https://developer.apple.com/documentation/uikit/uikeyinput/1614457-hastext
Expand Down Expand Up @@ -116,23 +125,4 @@ internal interface IOSSkikoInput {
* Returned value must be in range between 0 and length of text (inclusive).
*/
fun positionFromPosition(position: Long, offset: Long): Long

object Empty : IOSSkikoInput {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not used anymore?

Copy link
Collaborator Author

@ASalavei ASalavei Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, It hasn't beed used before as well.

override fun beginFloatingCursor(offset: DpOffset) = Unit
override fun updateFloatingCursor(offset: DpOffset) = Unit
override fun endFloatingCursor() = Unit
override fun hasText(): Boolean = false
override fun insertText(text: String) = Unit
override fun deleteBackward() = Unit
override fun endOfDocument(): Long = 0L
override fun getSelectedTextRange(): IntRange? = null
override fun setSelectedTextRange(range: IntRange?) = Unit
override fun selectAll() = Unit
override fun textInRange(range: IntRange): String = ""
override fun replaceRange(range: IntRange, text: String) = Unit
override fun setMarkedText(markedText: String?, selectedRange: IntRange) = Unit
override fun markedTextRange(): IntRange? = null
override fun unmarkText() = Unit
override fun positionFromPosition(position: Long, offset: Long): Long = 0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ internal class UIKitTextInputService(
}

override fun stopInput() {
flushEditCommandsIfNeeded(force = true)
currentInput = null
_tempCurrentInputSession = null
currentImeOptions = null
Expand Down Expand Up @@ -263,10 +264,27 @@ internal class UIKitTextInputService(
return event.type == KeyEventType.KeyDown
}

private val editCommandsBatch = mutableListOf<EditCommand>()
private var editBatchDepth: Int = 0
set(value) {
field = value
flushEditCommandsIfNeeded()
}

private fun sendEditCommand(vararg commands: EditCommand) {
val commandList = commands.toList()
_tempCurrentInputSession?.apply(commandList)
currentInput?.onEditCommand?.invoke(commandList)
_tempCurrentInputSession?.apply(commands.toList())

editCommandsBatch.addAll(commands)
flushEditCommandsIfNeeded()
}

fun flushEditCommandsIfNeeded(force: Boolean = false) {
if ((force || editBatchDepth == 0) && editCommandsBatch.isNotEmpty()) {
val commandList = editCommandsBatch.toList()
editCommandsBatch.clear()

currentInput?.onEditCommand?.invoke(commandList)
}
}

private fun getCursorPos(): Int? {
Expand Down Expand Up @@ -395,6 +413,14 @@ internal class UIKitTextInputService(
floatingCursorTranslation = null
}

override fun beginEditBatch() {
editBatchDepth++
}

override fun endEditBatch() {
editBatchDepth--
igordmn marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* A Boolean value that indicates whether the text-entry object has any text.
* https://developer.apple.com/documentation/uikit/uikeyinput/1614457-hastext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ internal class ComposeSceneMediator(
}

fun render(canvas: Canvas, nanoTime: Long) {
textInputService.flushEditCommandsIfNeeded(force = true)
scene.render(canvas, nanoTime)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import kotlin.time.DurationUnit
import kotlinx.cinterop.CValue
import kotlinx.cinterop.readValue
import kotlinx.cinterop.useContents
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import platform.CoreGraphics.CGPoint
import platform.CoreGraphics.CGRect
import platform.CoreGraphics.CGRectMake
Expand All @@ -47,6 +49,7 @@ import platform.UIKit.UIEvent
import platform.UIKit.UIKeyInputProtocol
import platform.UIKit.UIKeyboardAppearance
import platform.UIKit.UIKeyboardType
import platform.UIKit.UIPress
import platform.UIKit.UIPressesEvent
import platform.UIKit.UIReturnKeyType
import platform.UIKit.UITextAutocapitalizationType
Expand All @@ -66,7 +69,6 @@ import platform.UIKit.UITextRange
import platform.UIKit.UITextSelectionRect
import platform.UIKit.UITextStorageDirection
import platform.UIKit.UIView
import platform.UIKit.UIPress
import platform.darwin.NSInteger

private val NoOpOnKeyboardPresses: (Set<*>) -> Unit = {}
Expand All @@ -88,6 +90,8 @@ internal class IntermediateTextInputUIView(
}
}

private val mainScope = MainScope()

/**
* Callback to handle keyboard presses. The parameter is a [Set] of [UIPress] objects.
* Erasure happens due to K/N not supporting Obj-C lightweight generics.
Expand Down Expand Up @@ -233,7 +237,18 @@ internal class IntermediateTextInputUIView(
location.toInt() to length.toInt()
}
val relativeTextRange = locationRelative until locationRelative + lengthRelative
input?.setMarkedText(markedText, relativeTextRange)

// Due to iOS specifics, [setMarkedText] can be called several times in a row. Batching
// helps to avoid text input problems, when Composables use parameters set during
// recomposition instead of the current ones. Example:
// 1. State "1" -> TextField(text = "1")
// 2. setMarkedText "12" -> Not equal to TextField(text = "1") -> State "12"
// 3. setMarkedText "1" -> Equal to TextField(text = "1") -> State remains "12"
// scene.render() - Recomposes TextField
// 4. State "12" -> TextField(text = "12") - Invalid state. Should be TextField(text = "1")
input?.withBatch {
ASalavei marked this conversation as resolved.
Show resolved Hide resolved
input?.setMarkedText(markedText, relativeTextRange)
}
}

/**
Expand Down Expand Up @@ -496,6 +511,14 @@ internal class IntermediateTextInputUIView(
fun resetOnKeyboardPressesCallback() {
onKeyboardPresses = NoOpOnKeyboardPresses
}

private fun IOSSkikoInput.withBatch(update: () -> Unit) {
beginEditBatch()
update()
mainScope.launch {
endEditBatch()
}
}
}

private class IntermediateTextPosition(val position: Long = 0) : UITextPosition()
Expand Down