diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/BackingDomInput.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/BackingDomInput.kt index ec7397570eff6..372aa81495a70 100644 --- a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/BackingDomInput.kt +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/BackingDomInput.kt @@ -19,7 +19,9 @@ package androidx.compose.ui.platform import androidx.compose.ui.input.key.KeyEvent 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.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import kotlin.js.js import kotlinx.browser.document @@ -31,7 +33,9 @@ internal interface ComposeCommandCommunicator { fun sendEditCommand(command: EditCommand) = sendEditCommand(listOf(command)) fun sendKeyboardEvent(keyboardEvent: KeyEvent): Boolean +} +internal interface TextLayoutProvider { fun currentTextLayoutResult(): TextLayoutResult? } @@ -47,17 +51,19 @@ private fun setBackingInputBox(container: HTMLElement, left: Float, top: Float, * and the DOM HTMLTextAreaElement we are actually listening events on in order to show * the virtual keyboard. */ -internal class BackingDomInput( +internal abstract class BackingDomInput( val inputContainer: HTMLElement, imeOptions: ImeOptions, composeCommunicator : ComposeCommandCommunicator, -) { - private val inputStrategy = DomInputStrategy( - imeOptions, - composeCommunicator - ) +): TextLayoutProvider { + internal val backingElement = imeOptions.createDomElement() - internal val backingElement = inputStrategy.htmlInput + private val inputStrategy = object : DomInputStrategy( + backingElement, + composeCommunicator + ) { + override fun currentTextLayoutResult(): TextLayoutResult? = this@BackingDomInput.currentTextLayoutResult() + } fun register() { setBackingInputBox(container = inputContainer, 0f, 0f, 0f, 0f) @@ -99,3 +105,84 @@ internal class BackingDomInput( backingElement.remove() } } + + +private fun ImeOptions.createDomElement(): HTMLElement { + val htmlElement = document.createElement( + if (singleLine) "input" else "textarea" + ) as HTMLElement + + // without autocorrect set "on" iOS virtual keyboard won't suggest + // see https://youtrack.jetbrains.com/issue/CMP-8807 + htmlElement.setAttribute("autocorrect", "on") + htmlElement.setAttribute("autocomplete", "off") + htmlElement.setAttribute("autocapitalize", "off") + htmlElement.setAttribute("spellcheck", "false") + + val inputMode = when (keyboardType) { + KeyboardType.Text -> "text" + KeyboardType.Ascii -> "text" + KeyboardType.Number -> "number" + KeyboardType.Phone -> "tel" + KeyboardType.Uri -> "url" + KeyboardType.Email -> "email" + KeyboardType.Password -> "password" + KeyboardType.NumberPassword -> "number" + KeyboardType.Decimal -> "decimal" + else -> "text" + } + + val enterKeyHint = when (imeAction) { + ImeAction.Default -> "enter" + ImeAction.None -> "enter" + ImeAction.Done -> "done" + ImeAction.Go -> "go" + ImeAction.Next -> "next" + ImeAction.Previous -> "previous" + ImeAction.Search -> "search" + ImeAction.Send -> "send" + else -> "enter" + } + + htmlElement.setAttribute("inputmode", inputMode) + htmlElement.setAttribute("enterkeyhint", enterKeyHint) + + + htmlElement.style.apply { + setProperty("position", "absolute") + setProperty("user-select", "none") + setProperty("forced-color-adjust", "none") + setProperty("white-space", "pre") + setProperty("align-content", "center") + setProperty( + "top", + "calc(min(var(--compose-internal-web-backing-input-top) * 1px, 100vh - var(--compose-internal-web-backing-input-height) * 1px))" + ) + setProperty( + "left", + "calc(min(var(--compose-internal-web-backing-input-left) * 1px, 100vw - var(--compose-internal-web-backing-input-width) * 1px))" + ) + setProperty("width", "calc(var(--compose-internal-web-backing-input-width) * 1px") + setProperty("height", "calc(var(--compose-internal-web-backing-input-height) * 1px") + setProperty("padding", "0") + setProperty("color", "transparent") + setProperty("background", "transparent") + setProperty("caret-color", "transparent") + setProperty("outline", "none") + setProperty("border", "none") + setProperty("resize", "none") + setProperty("text-shadow", "none") + setProperty("z-index", "-1") + // TODO: do we need pointer-events: none + //setProperty("pointer-events", "none") + + // I keep "opacity" commented to make it explicit that we can't use this property. + // Reason: Safari iOS keyboard overlaps the text input. See CMP-8611 + // setProperty("opacity", "0") + + // To prevent auto-zoom in some mobile browsers, we set a larger font-size + setProperty("font-size", "20px") + } + + return htmlElement +} diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/DomInputStrategy.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/DomInputStrategy.kt index 4ad20d48a6449..8c4e47b99c2a1 100644 --- a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/DomInputStrategy.kt +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/DomInputStrategy.kt @@ -17,9 +17,7 @@ package androidx.compose.ui.platform import androidx.compose.ui.input.key.Key -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.TextLayoutResult import androidx.compose.ui.text.input.SetSelectionCommand import androidx.compose.ui.text.input.TextFieldValue import kotlin.js.ExperimentalWasmJsInterop @@ -38,12 +36,10 @@ import org.w3c.dom.events.UIEvent import org.w3c.dom.events.InputEvent import org.w3c.dom.events.KeyboardEvent -internal class DomInputStrategy( - imeOptions: ImeOptions, +internal abstract class DomInputStrategy( + private val htmlInput: HTMLElement, private val composeSender: ComposeCommandCommunicator, -) { - val htmlInput = imeOptions.createDomElement() - +): TextLayoutProvider { private var lastMeaningfulUpdate = TextFieldValue("") // To avoid the re-triggering of the selection change @@ -54,14 +50,6 @@ internal class DomInputStrategy( initEvents() } - private val nativeInputEventsProcessor = object : NativeInputEventsProcessor(composeSender) { - override fun scheduleCheckpoint() { - window.requestAnimationFrame { - runCheckpoint(currentTextFieldValue = lastMeaningfulUpdate) - } - } - } - fun updateState(textFieldValue: TextFieldValue) { htmlInput as HTMLElementWithValue @@ -82,9 +70,19 @@ internal class DomInputStrategy( private val tabKeyCode = Key.Tab.keyCode.toInt() private fun initEvents() { - htmlInput.addEventListener("blur", { evt -> - // TODO: any actions here? - }) + val nativeInputEventsProcessor = object : NativeInputEventsProcessor() { + override fun scheduleCheckpoint() { + window.requestAnimationFrame { + runCheckpoint(currentTextFieldValue = lastMeaningfulUpdate) + } + } + + override fun withCommandSenderContext(block: ComposeCommandCommunicator.() -> Unit) { + block.invoke(composeSender) + } + + override fun currentTextLayoutResult(): TextLayoutResult? = this@DomInputStrategy.currentTextLayoutResult() + } htmlInput.addEventListener("keydown", { evt -> nativeInputEventsProcessor.registerEvent(evt as KeyboardEvent) @@ -175,87 +173,6 @@ internal inline fun UIEvent.asInputEventExt(): InputEventExt = unsafeCast "text" - KeyboardType.Ascii -> "text" - KeyboardType.Number -> "number" - KeyboardType.Phone -> "tel" - KeyboardType.Uri -> "url" - KeyboardType.Email -> "email" - KeyboardType.Password -> "password" - KeyboardType.NumberPassword -> "number" - KeyboardType.Decimal -> "decimal" - else -> "text" - } - - val enterKeyHint = when (imeAction) { - ImeAction.Default -> "enter" - ImeAction.None -> "enter" - ImeAction.Done -> "done" - ImeAction.Go -> "go" - ImeAction.Next -> "next" - ImeAction.Previous -> "previous" - ImeAction.Search -> "search" - ImeAction.Send -> "send" - else -> "enter" - } - - htmlElement.setAttribute("inputmode", inputMode) - htmlElement.setAttribute("enterkeyhint", enterKeyHint) - - - htmlElement.style.apply { - setProperty("position", "absolute") - setProperty("user-select", "none") - setProperty("forced-color-adjust", "none") - setProperty("white-space", "pre") - setProperty("align-content", "center") - setProperty( - "top", - "calc(min(var(--compose-internal-web-backing-input-top) * 1px, 100vh - var(--compose-internal-web-backing-input-height) * 1px))" - ) - setProperty( - "left", - "calc(min(var(--compose-internal-web-backing-input-left) * 1px, 100vw - var(--compose-internal-web-backing-input-width) * 1px))" - ) - setProperty("width", "calc(var(--compose-internal-web-backing-input-width) * 1px") - setProperty("height", "calc(var(--compose-internal-web-backing-input-height) * 1px") - setProperty("padding", "0") - setProperty("color", "transparent") - setProperty("background", "transparent") - setProperty("caret-color", "transparent") - setProperty("outline", "none") - setProperty("border", "none") - setProperty("resize", "none") - setProperty("text-shadow", "none") - setProperty("z-index", "-1") - // TODO: do we need pointer-events: none - //setProperty("pointer-events", "none") - - // I keep "opacity" commented to make it explicit that we can't use this property. - // Reason: Safari iOS keyboard overlaps the text input. See CMP-8611 - // setProperty("opacity", "0") - - // To prevent auto-zoom in some mobile browsers, we set a larger font-size - setProperty("font-size", "20px") - } - - return htmlElement -} - private external interface HTMLElementWithValue { var value: String val selectionStart: Int diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/NativeInputEventsProcessor.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/NativeInputEventsProcessor.kt index 4e8b8bdc44b19..848693a7033ae 100644 --- a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/NativeInputEventsProcessor.kt +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/NativeInputEventsProcessor.kt @@ -44,12 +44,12 @@ import org.w3c.dom.events.UIEvent * @param composeSender The communicator responsible for transmitting edit * commands and keyboard events to the Compose system. */ -internal abstract class NativeInputEventsProcessor( - private val composeSender: ComposeCommandCommunicator -) { +internal abstract class NativeInputEventsProcessor : TextLayoutProvider { private val collectedEvents = mutableListOf() + internal abstract fun withCommandSenderContext(block: ComposeCommandCommunicator.() -> Unit) + @get:TestOnly @set:TestOnly internal var isCheckpointScheduled = false @@ -57,16 +57,19 @@ internal abstract class NativeInputEventsProcessor( internal var lastCompositionEndTimestamp = 0.0 // Double because of k/wasm where Number.toLong() leads to a compilation error private var lastProcessedKeydown: KeyboardEvent? = null - private tailrec fun TextLayoutResult.getPrevWordOffset(currentOffset: Int): Int { + private tailrec fun TextLayoutResult.getPrevWordOffset( + currentOffset: Int, + offsetMapping: OffsetMapping = OffsetMapping.Identity + ): Int { if (currentOffset <= 0) { return 0 } val text = layoutInput.text - val currentWord = getWordBoundary(currentOffset.coerceIn(0, text.length - 1)) + val currentWord = getWordBoundary(currentOffset.coerceAtMost(text.length - 1)) return if (currentWord.start >= currentOffset) { getPrevWordOffset(currentOffset - 1) } else { - OffsetMapping.Identity.transformedToOriginal(currentWord.start) + offsetMapping.transformedToOriginal(currentWord.start) } } @@ -87,7 +90,7 @@ internal abstract class NativeInputEventsProcessor( } } - fun runCheckpoint(currentTextFieldValue: TextFieldValue) { + fun runCheckpoint(currentTextFieldValue: TextFieldValue) = withCommandSenderContext { isCheckpointScheduled = false collectedEvents.sortBy { it.timeStamp.toInt() } @@ -126,7 +129,7 @@ internal abstract class NativeInputEventsProcessor( val shouldBeProcessed = timestamp == 0.0 || !isFromLastComposition if (shouldBeProcessed) { - val isProcessed = composeSender.sendKeyboardEvent(evt.toComposeEvent()) + val isProcessed = sendKeyboardEvent(evt.toComposeEvent()) if (isProcessed) { lastProcessedKeydown = evt } @@ -135,7 +138,7 @@ internal abstract class NativeInputEventsProcessor( "compositionend" -> { lastCompositionEndTimestamp = timestamp - composeSender.sendEditCommand(CommitTextCommand((evt as CompositionEvent).data, 1)) + sendEditCommand(CommitTextCommand((evt as CompositionEvent).data, 1)) } "beforeinput" -> { @@ -149,83 +152,89 @@ internal abstract class NativeInputEventsProcessor( collectedEvents.clear() } - private fun InputEventExt.process(currentTextFieldValue: TextFieldValue) { - val editCommands = when (inputType) { - "deleteContentBackward" -> buildList { - // this means "deleteContentBackward" happened because of an earlier "keydown" event, so skipping it here - if (lastProcessedKeydown?.isBackspace() == true) return@buildList - - if (!currentTextFieldValue.selection.collapsed) { - // Likely it's on mobile, where the Backspace has Unidentified key value. - // When Compose TextField shows text selection, - // a good UX for deleteContentBackward would be to emulate Backspace - add(BackspaceCommand()) - } else { - // This happens when an autocorrection is applied on mobile: - // The system first tells us to delete the old text, - // and then it would send the "insertText" event. - if (textRangeSize > 0) { - // deleteContentBackward can happen under very non-trivial circumstances, - // for instance; when an input suggestion on Android Chrome is accepted, - // the browser then deletes space after the word just to add space again - add(SetSelectionCommand(textRangeStart, textRangeEnd)) - add(BackspaceCommand()) - } else if (textRangeSize == 0) { - // under specific circumstance previous symbol can be deleted while inputing new one - // see https://youtrack.jetbrains.com/issue/CMP-8773 + private fun InputEventExt.process(currentTextFieldValue: TextFieldValue) = withCommandSenderContext { + val editCommands = buildList { + when (inputType) { + "deleteContentBackward" -> { + // this means "deleteContentBackward" happened because of an earlier "keydown" event, so skipping it here + if (lastProcessedKeydown?.isBackspace() == true) return@buildList + + if (!currentTextFieldValue.selection.collapsed) { + // Likely it's on mobile, where the Backspace has Unidentified key value. + // When Compose TextField shows text selection, + // a good UX for deleteContentBackward would be to emulate Backspace add(BackspaceCommand()) + } else { + // This happens when an autocorrection is applied on mobile: + // The system first tells us to delete the old text, + // and then it would send the "insertText" event. + if (textRangeSize > 0) { + // deleteContentBackward can happen under very non-trivial circumstances, + // for instance; when an input suggestion on Android Chrome is accepted, + // the browser then deletes space after the word just to add space again + add(SetSelectionCommand(textRangeStart, textRangeEnd)) + add(BackspaceCommand()) + } else if (textRangeSize == 0) { + // under specific circumstance previous symbol can be deleted while inputing new one + // see https://youtrack.jetbrains.com/issue/CMP-8773 + add(BackspaceCommand()) + } } } - } - "deleteWordBackward" -> buildList { - if (lastProcessedKeydown?.isBackspace() != true) return@buildList + "deleteWordBackward" -> { + if (lastProcessedKeydown?.isBackspace() != true) return@buildList + + // This would mean event was triggered by long press on mobile device (iOS) + if (lastProcessedKeydown?.repeat == true) { + val layoutResult = + currentTextLayoutResult() ?: return@buildList + + val offset = layoutResult.getPrevWordOffset( + OffsetMapping.Identity.originalToTransformed(textRangeEnd) + ) + val deleteCommand = DeleteSurroundingTextCommand( + (textRangeEnd - offset).coerceAtLeast(0), + 0 + ) + add(deleteCommand) + } + } - // This would mean event was triggered by long press on mobile device (iOS) - if (lastProcessedKeydown?.repeat == true) { - val layoutResult = composeSender.currentTextLayoutResult() ?: return@buildList + "insertReplacementText" -> { + if (data == null) return@buildList + if (textRangeSize > 0) { + add(SetSelectionCommand(textRangeStart, textRangeEnd)) + } - val offset = layoutResult.getPrevWordOffset(textRangeStart) - val deleteCommand = DeleteSurroundingTextCommand((textRangeEnd - offset).coerceAtLeast(0), 0) - add(deleteCommand) + add(CommitTextCommand(data, 1)) } - } + "insertText" -> { + if (data == null) return@buildList + if (textRangeSize > 0 && currentTextFieldValue.selection.collapsed) { + add(SetSelectionCommand(textRangeStart, textRangeEnd)) + } - "insertReplacementText" -> buildList { - if (data == null) return@buildList - if (textRangeSize > 0) { - add(SetSelectionCommand(textRangeStart, textRangeEnd)) + add(CommitTextCommand(data, 1)) } - add(CommitTextCommand(data, 1)) - } - - "insertText" -> buildList { - if (data == null) return@buildList - if (textRangeSize > 0 && currentTextFieldValue.selection.collapsed) { - add(SetSelectionCommand(textRangeStart, textRangeEnd)) + "insertCompositionText" -> { + if (data == null) return@buildList + if (textRangeSize > 0) { + add(SetSelectionCommand(textRangeStart, textRangeEnd)) + } + add(SetComposingTextCommand(data, 1)) } - add(CommitTextCommand(data, 1)) + // "insertFromComposition", "deleteCompositionText" are triggered in Safari just before the 'compositionEnd' event. + // They're ignored because Safari also sends 'insertCompositionText' which we handle (alongside 'compositionEnd') } - - "insertCompositionText" -> buildList { - if (data == null) return@buildList - if (textRangeSize > 0) { - add(SetSelectionCommand(textRangeStart, textRangeEnd)) - } - add(SetComposingTextCommand(data, 1)) - } - - // "insertFromComposition", "deleteCompositionText" are triggered in Safari just before the 'compositionEnd' event. - // They're ignored because Safari also sends 'insertCompositionText' which we handle (alongside 'compositionEnd') - else -> emptyList() } if (editCommands.isNotEmpty()) { - composeSender.sendEditCommand(editCommands) + sendEditCommand(editCommands) } } diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/WebTextInputService.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/WebTextInputService.kt index 8047801e10c30..34da010542925 100644 --- a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/WebTextInputService.kt +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/platform/WebTextInputService.kt @@ -51,6 +51,8 @@ internal abstract class WebTextInputService : field = value } + internal var currentTextLayoutResult: (() -> TextLayoutResult?)? = null + /** * It's used for the initial positioning of the backing HTML input. * It's rather a workaround for the problem that startInput doesn't know the correct position yet. @@ -65,12 +67,14 @@ internal abstract class WebTextInputService : */ abstract val backingDomInputContainer: HTMLElement - fun startInput( - request: PlatformTextInputMethodRequest, + override fun startInput( + value: TextFieldValue, + imeOptions: ImeOptions, onEditCommand: (List) -> Unit, + onImeActionPerformed: (ImeAction) -> Unit ) { - backingDomInput = BackingDomInput( - imeOptions = request.imeOptions, + backingDomInput = object : BackingDomInput( + imeOptions = imeOptions, composeCommunicator = object : ComposeCommandCommunicator { override fun sendKeyboardEvent(keyboardEvent: KeyEvent): Boolean { return this@WebTextInputService.processKeyboardEvent(keyboardEvent) @@ -79,11 +83,13 @@ internal abstract class WebTextInputService : override fun sendEditCommand(commands: List) { onEditCommand(commands) } - - override fun currentTextLayoutResult() = request.textLayoutResult() }, inputContainer = backingDomInputContainer, - ) + ) { + override fun currentTextLayoutResult(): TextLayoutResult? = + this@WebTextInputService.currentTextLayoutResult?.invoke() + } + backingDomInput?.register() if (currentTouchOffset != null) { @@ -96,21 +102,12 @@ internal abstract class WebTextInputService : showSoftwareKeyboard() } - override fun startInput( - value: TextFieldValue, - imeOptions: ImeOptions, - onEditCommand: (List) -> Unit, - onImeActionPerformed: (ImeAction) -> Unit - ) { - // This method is called from the common code. - // It's not used in the new API, but we keep it for backward compatibility. - } - fun getBackingInput(): HTMLElement? { return backingDomInput?.backingElement?.takeIf { it.isConnected } } override fun stopInput() { + currentTextLayoutResult = null backingDomInput?.dispose() backingDomInput = null } diff --git a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/WebTextInputSession.kt b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/WebTextInputSession.kt index c9850bc17f9cd..064470b419a4e 100644 --- a/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/WebTextInputSession.kt +++ b/compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/WebTextInputSession.kt @@ -51,10 +51,14 @@ internal class WebTextInputSession( } suspendCancellableCoroutine { continuation -> webTextInputService.startInput( - request, - onEditCommand = request.onEditCommand + value = request.value(), + imeOptions = request.imeOptions, + onEditCommand = request.onEditCommand, + onImeActionPerformed = request.onImeAction ?: {} ) + webTextInputService.currentTextLayoutResult = request.textLayoutResult + continuation.invokeOnCancellation { webTextInputService.stopInput() } diff --git a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/platform/NativeInputEventsProcessorTest.kt b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/platform/NativeInputEventsProcessorTest.kt index 78139383036f0..c7dd8c0de2542 100644 --- a/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/platform/NativeInputEventsProcessorTest.kt +++ b/compose/ui/ui/src/webTest/kotlin/androidx/compose/ui/platform/NativeInputEventsProcessorTest.kt @@ -83,37 +83,6 @@ class NativeInputEventsProcessorTest { return true } - override fun currentTextLayoutResult(): TextLayoutResult? { - val text = editingBuffer.toString() - val annotatedString = AnnotatedString(text) - val density = Density(1f) - val constraints = Constraints() - val style = TextStyle.Default - - return TextLayoutResult( - layoutInput = TextLayoutInput( - text = annotatedString, - style = style, - placeholders = emptyList(), - maxLines = Int.MAX_VALUE, - softWrap = true, - overflow = TextOverflow.Clip, - density = density, - layoutDirection = LayoutDirection.Ltr, - fontFamilyResolver = fontFamilyResolver, - constraints = constraints - ), - multiParagraph = MultiParagraph( - annotatedString = annotatedString, - style = style, - constraints = constraints, - density = density, - fontFamilyResolver = fontFamilyResolver - ), - size = IntSize(0, 0) - ) - } - @Suppress("INVISIBLE_REFERENCE") fun currentTextFieldValue(): TextFieldValue { return TextFieldValue( @@ -127,8 +96,8 @@ class NativeInputEventsProcessorTest { * A test implementation of NativeInputEventsProcessor that allows controlling when checkpoints are run */ private class TestNativeInputEventsProcessor( - composeSender: ComposeCommandCommunicator - ) : NativeInputEventsProcessor(composeSender) { + private val composeSender: ComposeCommandCommunicator + ) : NativeInputEventsProcessor() { override fun scheduleCheckpoint() { isCheckpointScheduled = true @@ -138,6 +107,14 @@ class NativeInputEventsProcessorTest { isCheckpointScheduled = false runCheckpoint(currentTextFieldValue) } + + override fun withCommandSenderContext(block: ComposeCommandCommunicator.() -> Unit) { + block.invoke(composeSender) + } + + override fun currentTextLayoutResult(): TextLayoutResult? { + TODO("currentTextLayoutResult is tested in integration tests") + } } @Test