From ae0fd0d692a315ff5b11617dffe60e115bee396e Mon Sep 17 00:00:00 2001 From: Shagen Ogandzhanian Date: Fri, 5 Apr 2024 16:39:19 +0200 Subject: [PATCH] [wasm][js] Dispose all listened events alongside with the application being disposed (#1239) --- .../ui/test/InputDispatcher.wasmMain.kt | 48 ++++--------------- .../ui/events/DisposableEventListener.kt | 42 ++++++++++++++++ .../compose/ui/window/ComposeWindow.js.kt | 34 +++++++++---- 3 files changed, 76 insertions(+), 48 deletions(-) create mode 100644 compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/events/DisposableEventListener.kt diff --git a/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt b/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt index 71b069d849bcb..c3eab868b850d 100644 --- a/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt +++ b/compose/ui/ui-test/src/wasmJsMain/kotlin/androidx/compose/ui/test/InputDispatcher.wasmMain.kt @@ -19,12 +19,7 @@ package androidx.compose.ui.test import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.KeyEventType -import org.jetbrains.skiko.SkikoInputModifiers -import org.jetbrains.skiko.SkikoKey -import org.jetbrains.skiko.SkikoKeyboardEvent -import org.jetbrains.skiko.SkikoKeyboardEventKind -import org.w3c.dom.events.KeyboardEvent -import org.w3c.dom.events.KeyboardEventInit +import androidx.compose.ui.input.key.NativeKeyEvent /** * The [KeyEvent] is usually created by the system. This function creates an instance of @@ -35,26 +30,13 @@ internal actual fun keyEvent( ): KeyEvent { val nativeCode = key.keyCode.toInt() - val kind = when (keyEventType) { - KeyEventType.KeyUp -> SkikoKeyboardEventKind.UP - KeyEventType.KeyDown -> SkikoKeyboardEventKind.DOWN - else -> SkikoKeyboardEventKind.UNKNOWN - } - return KeyEvent( - SkikoKeyboardEvent( - kind = kind, - key = SkikoKey.valueOf(nativeCode), - modifiers = SkikoInputModifiers(modifiers), - platform = KeyboardEvent( - when (kind) { - SkikoKeyboardEventKind.DOWN -> "keydown" - SkikoKeyboardEventKind.UP -> "keyup" - else -> "keypress" - }, KeyboardEventInit( - key = if (key.isPrintable()) nativeCode.toChar().toString() else "Unknown" - ) - ) + NativeKeyEvent( + key = key, + kind = keyEventType, + value = if (key.isPrintable()) nativeCode.toChar().toString() else "Unknown", + modifiers = modifiers, + timestamp = 0 ) ) } @@ -79,18 +61,4 @@ private fun Key.isPrintable(): Boolean { return !NonPrintableKeys.contains(this) } -internal actual fun Int.updatedKeyboardModifiers(key: Key, down: Boolean): Int { - val mask = when (key) { - Key.ShiftLeft, Key.ShiftRight -> SkikoInputModifiers.SHIFT.value - Key.CtrlLeft, Key.CtrlRight -> SkikoInputModifiers.CONTROL.value - Key.MetaLeft, Key.MetaRight -> SkikoInputModifiers.META.value - Key.AltLeft, Key.AltRight -> SkikoInputModifiers.ALT.value - else -> null - } - - return if (mask != null) { - if (down) this or mask else this xor mask - } else { - this - } -} \ No newline at end of file +internal actual fun Int.updatedKeyboardModifiers(key: Key, down: Boolean): Int = this \ No newline at end of file diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/events/DisposableEventListener.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/events/DisposableEventListener.kt new file mode 100644 index 0000000000000..7d9969b2ba2d0 --- /dev/null +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/events/DisposableEventListener.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2024 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.events + +import org.w3c.dom.events.Event +import org.w3c.dom.events.EventTarget + +private fun interface DisposableEventListener { + fun dispose() +} + +private fun EventTarget.addDisposableEvent(eventName: String, handler: (Event) -> Unit): DisposableEventListener { + addEventListener(eventName, handler) + return DisposableEventListener { removeEventListener(eventName, handler) } +} + +internal class EventTargetListener(private val eventTarget: EventTarget): DisposableEventListener { + private val registeredEvents = mutableListOf() + + fun addDisposableEvent(eventName: String, handler: (Event) -> Unit) { + registeredEvents.add(eventTarget.addDisposableEvent(eventName, handler)) + } + + override fun dispose() { + registeredEvents.forEach { evt -> evt.dispose() } + } +} + diff --git a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.js.kt b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.js.kt index 7ee584aeb37ac..4c3a5e504054f 100644 --- a/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.js.kt +++ b/compose/ui/ui/src/webCommonW3C/kotlin/androidx/compose/ui/window/ComposeWindow.js.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.LocalSystemTheme +import androidx.compose.ui.events.EventTargetListener import androidx.compose.ui.events.toNativeDragEvent import androidx.compose.ui.events.toNativePointerEvent import androidx.compose.ui.events.toNativeScrollEvent @@ -72,9 +73,16 @@ private interface ComposeWindowState { fun init() {} fun sizeFlow(): Flow + val globalEvents: EventTargetListener + + fun dispose() { + globalEvents.dispose() + } + companion object { fun createFromLambda(lambda: suspend () -> IntSize): ComposeWindowState { return object : ComposeWindowState { + override val globalEvents = EventTargetListener(window) override fun sizeFlow(): Flow = flow { while (coroutineContext.isActive) { emit(lambda()) @@ -88,10 +96,13 @@ private interface ComposeWindowState { private class DefaultWindowState(private val viewportContainer: Element) : ComposeWindowState { private val channel = Channel(CONFLATED) + override val globalEvents = EventTargetListener(window) + override fun init() { - window.addEventListener("resize", { + + globalEvents.addDisposableEvent("resize") { channel.trySend(getParentContainerBox()) - }) + } initMediaEventListener { channel.trySend(getParentContainerBox()) @@ -133,6 +144,9 @@ private class ComposeWindow( private val _windowInfo = WindowInfoImpl().apply { isWindowFocused = true } + + private val canvasEvents = EventTargetListener(canvas) + private val jsTextInputService = JSTextInputService() private val platformContext: PlatformContext = object : PlatformContext by PlatformContext.Empty { @@ -163,7 +177,7 @@ private class ComposeWindow( type: String, handler: (event: T) -> Unit ) { - canvas.addEventListener(type, { event -> handler(event as T) }) + canvasEvents.addDisposableEvent(type) { event -> handler(event as T) } } private fun initEvents(canvas: HTMLCanvasElement) { @@ -234,13 +248,13 @@ private class ComposeWindow( if (processed) event.preventDefault() } - window.addEventListener("focus", { + state.globalEvents.addDisposableEvent("focus") { lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) - }) + } - window.addEventListener("blur", { + state.globalEvents.addDisposableEvent("blur") { lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) - }) + } lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) } @@ -296,6 +310,10 @@ private class ComposeWindow( lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) layer.dispose() systemThemeObserver.dispose() + state.dispose() + // modern browsers supposed to garbage collect all events on the element disposed + // but actually we never can be sure dom element was collected in first place + canvasEvents.dispose() } } @@ -385,4 +403,4 @@ fun ComposeViewport( content = content, state = DefaultWindowState(viewportContainer) ) -} +} \ No newline at end of file