Skip to content

Commit

Permalink
Sending KeyEvents from IME (#1297)
Browse files Browse the repository at this point in the history
## Release Notes
### Features - Web
- Basic IME keyboard support
  • Loading branch information
Schahen committed Apr 26, 2024
1 parent 0f61ec3 commit a82ef0f
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
Expand Down Expand Up @@ -108,7 +109,7 @@ private fun TextBlock(
OutlinedTextField(
value = textState.value,
onValueChange = { textState.value = it },
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
modifier = Modifier.fillMaxWidth().padding(top = 8.dp).onKeyEvent { println("KEY EVENT $this"); false },
textStyle = TextStyle(fontSize = 12.sp, fontWeight = FontWeight.Normal),
keyboardOptions = KeyboardOptions(imeAction = imeActionName),
keyboardActions = keyboardActions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ internal fun KeyboardEvent.toComposeEvent(): KeyEvent {
nativeKeyEvent = InternalKeyEvent(
key = Key(keyCode.toLong(), location),
type = when (type) {
"keydown", "keypress" -> KeyEventType.KeyDown
"keydown" -> KeyEventType.KeyDown
"keyup" -> KeyEventType.KeyUp
else -> KeyEventType.Unknown
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,31 @@
package androidx.compose.ui.platform

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.toComposeEvent
import androidx.compose.ui.text.input.CommitTextCommand
import androidx.compose.ui.text.input.DeleteAllCommand
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.SetComposingTextCommand
import androidx.compose.ui.text.input.TextFieldValue
import kotlinx.browser.document
import org.w3c.dom.HTMLTextAreaElement
import org.w3c.dom.events.KeyboardEvent
import org.w3c.dom.events.KeyboardEventInit

/**
* The purpose of this entity is to isolate synchronization between a TextFieldValue
* and the DOM HTMLTextAreaElement we are actually listening events on in order to show
* the virtual keyboard.
*/
* The purpose of this entity is to isolate synchronization between a TextFieldValue
* and the DOM HTMLTextAreaElement we are actually listening events on in order to show
* the virtual keyboard.
*/
internal class BackingTextArea(
private val imeOptions: ImeOptions,
private val onEditCommand: (List<EditCommand>) -> Unit,
private val onImeActionPerformed: (ImeAction) -> Unit
private val onImeActionPerformed: (ImeAction) -> Unit,
private val sendKey: (evt: KeyEvent) -> Unit
) {
private val textArea: HTMLTextAreaElement = createHtmlInput()

Expand Down Expand Up @@ -95,62 +100,58 @@ internal class BackingTextArea(
setProperty("text-shadow", "none")
}

htmlInput.addEventListener("input", {
val text = htmlInput.value
val cursorPosition = htmlInput.selectionEnd
sendImeValueToCompose(onEditCommand, text, cursorPosition)
htmlInput.addEventListener("input", { evt ->
evt.preventDefault()
evt as InputEventExtended

when (evt.inputType) {
"insertLineBreak" -> {
if (imeOptions.singleLine) {
onImeActionPerformed(imeOptions.imeAction)
}
}

"insertCompositionText" -> {
val data = evt.data ?: return@addEventListener
onEditCommand(listOf(SetComposingTextCommand(data, 1)))
}

"insertText" -> {
val data = evt.data ?: return@addEventListener
if (data.length == 1) {
sendKey(
KeyboardEvent(
"keydown", KeyboardEventInit(key = data)
).toComposeEvent()
)
} else if (data.length > 1) {
onEditCommand(listOf(CommitTextCommand(data, 1)))
}
}

"deleteContentBackward" -> {
sendKey(
KeyboardEvent(
"keydown",
KeyboardEventInit(key = "Backspace", code = "Backspace").withKeyCode(Key.Backspace)
).toComposeEvent()
)
}
}
})

htmlInput.addEventListener("contextmenu", { evt ->
evt.preventDefault()
evt.stopPropagation()
})

// this done by analogy with KeyCommand.NEW_LINE processing in TextFieldKeyInput
if (imeOptions.singleLine) {
htmlInput.addEventListener("keydown", { evt ->
evt.preventDefault()
evt as KeyboardEvent
if (evt.key == "Enter" && evt.type == "keydown") {
onImeActionPerformed(imeOptions.imeAction)
}
})
}

return htmlInput
}

fun register() {
document.body?.appendChild(textArea)
}

private fun sendImeValueToCompose(
onEditCommand: (List<EditCommand>) -> Unit,
text: String,
newCursorPosition: Int? = null
) {
val value = if (text == "\n") {
""
} else {
text
}

if (newCursorPosition != null) {
onEditCommand(
listOf(
DeleteAllCommand(),
CommitTextCommand(value, newCursorPosition),
)
)
} else {
onEditCommand(
listOf(
CommitTextCommand(value, 1)
)
)
}
}

fun focus() {
textArea.focus()
}
Expand All @@ -174,4 +175,20 @@ internal class BackingTextArea(
fun dispose() {
textArea.remove()
}
}
}

private external interface InputEventExtended {
val inputType: String
val data: String?
}

// TODO: reuse in tests
private external interface KeyboardEventInitExtended : KeyboardEventInit {
var keyCode: Int?
}

@Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE")
private fun KeyboardEventInit.withKeyCode(key: Key) =
(this as KeyboardEventInitExtended).apply {
this.keyCode = key.keyCode.toInt()
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ internal class WebImeInputService(parentInputService: InputAwareInputService) :
onEditCommand: (List<EditCommand>) -> Unit,
onImeActionPerformed: (ImeAction) -> Unit
) {
backingTextArea = BackingTextArea(imeOptions, onEditCommand, onImeActionPerformed)
backingTextArea =
BackingTextArea(imeOptions, onEditCommand, onImeActionPerformed, sendKey = { evt ->
sendKeyEvent(evt)
})
backingTextArea?.register()

showSoftwareKeyboard()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package androidx.compose.ui.platform

import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.input.InputMode
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
Expand All @@ -27,6 +27,7 @@ import androidx.compose.ui.text.input.TextFieldValue

internal interface InputAwareInputService {
fun getOffset(rect: Rect): Offset
fun sendKeyEvent(event: KeyEvent)
fun isVirtualKeyboard(): Boolean
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.compose.ui.events.EventTargetListener
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.input.InputModeManager
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.input.key.toComposeEvent
import androidx.compose.ui.input.pointer.BrowserCursor
import androidx.compose.ui.input.pointer.PointerEventType
Expand Down Expand Up @@ -108,10 +109,8 @@ internal interface ComposeWindowState {
}

private sealed interface KeyboardModeState {
companion object {
object Virtual: KeyboardModeState
object Hardware: KeyboardModeState
}
object Virtual: KeyboardModeState
object Hardware: KeyboardModeState
}

internal class DefaultWindowState(private val viewportContainer: Element) : ComposeWindowState {
Expand Down Expand Up @@ -168,22 +167,26 @@ internal class ComposeWindow(

private val canvasEvents = EventTargetListener(canvas)

private var keyboardModeState: KeyboardModeState = KeyboardModeState.Companion.Hardware
private var keyboardModeState: KeyboardModeState = KeyboardModeState.Hardware

private val platformContext: PlatformContext = object : PlatformContext {
override val windowInfo get() = _windowInfo

override val inputModeManager: InputModeManager = DefaultInputModeManager()

override val textInputService = object : WebTextInputService() {
override fun isVirtualKeyboard() = keyboardModeState == KeyboardModeState.Companion.Virtual
override fun isVirtualKeyboard() = keyboardModeState == KeyboardModeState.Virtual

override fun getOffset(rect: Rect): Offset {
val viewportRect = canvas.getBoundingClientRect()
val offsetX = viewportRect.left.toFloat().coerceAtLeast(0f) + (rect.left / density.density)
val offsetY = viewportRect.top.toFloat().coerceAtLeast(0f) + (rect.top / density.density)
return Offset(offsetX, offsetY)
}

override fun sendKeyEvent(event: KeyEvent) {
layer.onKeyboardEvent(event)
}
}

override val viewConfiguration =
Expand Down Expand Up @@ -354,7 +357,7 @@ internal class ComposeWindow(
event: TouchEvent,
offset: Offset,
) {
keyboardModeState = KeyboardModeState.Companion.Virtual
keyboardModeState = KeyboardModeState.Virtual
val eventType = when (event.type) {
"touchstart" -> PointerEventType.Press
"touchmove" -> PointerEventType.Move
Expand Down Expand Up @@ -386,7 +389,7 @@ internal class ComposeWindow(
private fun ComposeLayer.onMouseEvent(
event: MouseEvent,
) {
keyboardModeState = KeyboardModeState.Companion.Hardware
keyboardModeState = KeyboardModeState.Hardware
val eventType = when (event.type) {
"mousedown" -> PointerEventType.Press
"mousemove" -> PointerEventType.Move
Expand All @@ -413,7 +416,7 @@ internal class ComposeWindow(
private fun ComposeLayer.onWheelEvent(
event: WheelEvent,
) {
keyboardModeState = KeyboardModeState.Companion.Hardware
keyboardModeState = KeyboardModeState.Hardware
onMouseEvent(
eventType = PointerEventType.Scroll,
position = event.offset,
Expand Down

0 comments on commit a82ef0f

Please sign in to comment.