diff --git a/ChangeLog.md b/ChangeLog.md index 4e9a20cd43fca..d373352afbf1b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,8 @@ See docs/process.md for more on how version tagging works. 4.0.20 (in development) ----------------------- +- Added `emscripten_html5_remove_event_listener` function in `html5.h` in order to be + able to remove a single callback. (#25535) - The standalone `file_packager.py` script no longer supports `--embed` with JS output (use `--obj-output` is now required for embedding data). This usage has been producing a warning since #16050 which is now an error. (#25049) diff --git a/site/source/docs/api_reference/html5.h.rst b/site/source/docs/api_reference/html5.h.rst index 23155eefd4307..fee237551da37 100644 --- a/site/source/docs/api_reference/html5.h.rst +++ b/site/source/docs/api_reference/html5.h.rst @@ -90,6 +90,54 @@ The ``useCapture`` parameter maps to ``useCapture`` in `EventTarget.addEventLis Most functions return the result using the type :c:data:`EMSCRIPTEN_RESULT`. Zero and positive values denote success. Negative values signal failure. None of the functions fail or abort by throwing a JavaScript or C++ exception. If a particular browser does not support the given feature, the value :c:data:`EMSCRIPTEN_RESULT_NOT_SUPPORTED` will be returned at the time the callback is registered. +Unregister function +------------------- + +In order to unregister a single event handler callback, call the following function: + + .. code-block:: cpp + + EMSCRIPTEN_RESULT emscripten_html5_remove_event_listener( + const char *target, // ID of the target HTML element. + void *userData, // User-defined data (passed to the callback). + int eventTypeId, // The event type ID (EMSCRIPTEN_EVENT_XXX). + void *callback // Callback function. + ); + + +The ``target``, ``userData`` and ``callback`` parameters are the same parameters provided in ``emscripten_set_some_callback`` with the only difference being that, since this function applies to all types of callbacks, the type of ``callback`` is ``void *``. + +Note in particular that the value of ``userData`` will need to match with the call that was used to register the callback. If you are having trouble, double check the value of ``userData``. + +The ``eventTypeId`` represents the event type, the same Id received in the callback functions. + +The function returns ``EMSCRIPTEN_RESULT_SUCCESS`` when the event handler callback is removed and ``EMSCRIPTEN_RESULT_INVALID_PARAM`` otherwise. + + .. code-block:: cpp + + // Example + + bool my_mouse_callback_1(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) { + // ... + } + + bool my_mouse_callback_2(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) { + // ... + } + + void main() { + + // 1. set callbacks for mouse down and mouse move + emscripten_set_mousedown_callback("#mydiv", 0, my_mouse_callback_1); + emscripten_set_mousedown_callback("#mydiv", (void *) 34, my_mouse_callback_2); + emscripten_set_mousemove_callback("#mydiv", 0, my_mouse_callback_1); + + // 2. remove these callbacks + emscripten_html5_remove_event_listener("#mydiv", 0, EMSCRIPTEN_EVENT_MOUSEDOWN, my_mouse_callback_1); + emscripten_html5_remove_event_listener("#mydiv", (void *) 34, EMSCRIPTEN_EVENT_MOUSEDOWN, my_mouse_callback_2); + emscripten_html5_remove_event_listener("#mydiv", 0, EMSCRIPTEN_EVENT_MOUSEMOVE, my_mouse_callback_1); + } + Callback functions ------------------ diff --git a/src/lib/libhtml5.js b/src/lib/libhtml5.js index 67f073a7ff7d9..ca90bc05b7ebd 100644 --- a/src/lib/libhtml5.js +++ b/src/lib/libhtml5.js @@ -201,6 +201,19 @@ var LibraryHTML5 = { return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}}; }, + removeSingleHandler(eventHandler) { + for (var [i, handler] of JSEvents.eventHandlers.entries()) { + if (handler.target === eventHandler.target + && handler.eventTypeId === eventHandler.eventTypeId + && handler.callbackfunc === eventHandler.callbackfunc + && handler.userData === eventHandler.userData) { + JSEvents._removeHandler(i); + return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}}; + } + } + return {{{ cDefs.EMSCRIPTEN_RESULT_INVALID_PARAM }}}; + }, + #if PTHREADS getTargetThreadForEventCallback(targetThread) { switch (targetThread) { @@ -291,6 +304,8 @@ var LibraryHTML5 = { var eventHandler = { target: findEventTarget(target), eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: keyEventHandlerFunc, useCapture @@ -405,6 +420,18 @@ var LibraryHTML5 = { }, #endif + emscripten_html5_remove_event_listener__proxy: 'sync', + emscripten_html5_remove_event_listener__deps: ['$JSEvents', '$findEventTarget'], + emscripten_html5_remove_event_listener: (target, userData, eventTypeId, callback) => { + var eventHandler = { + target: findEventTarget(target), + userData, + eventTypeId, + callbackfunc: callback, + }; + return JSEvents.removeSingleHandler(eventHandler); + }, + emscripten_set_keypress_callback_on_thread__proxy: 'sync', emscripten_set_keypress_callback_on_thread__deps: ['$registerKeyEventCallback'], emscripten_set_keypress_callback_on_thread: (target, userData, useCapture, callbackfunc, targetThread) => @@ -495,6 +522,8 @@ var LibraryHTML5 = { allowsDeferredCalls: eventTypeString != 'mousemove' && eventTypeString != 'mouseenter' && eventTypeString != 'mouseleave', // Mouse move events do not allow fullscreen/pointer lock requests to be handled in them! #endif eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: mouseEventHandlerFunc, useCapture @@ -586,6 +615,8 @@ var LibraryHTML5 = { allowsDeferredCalls: true, #endif eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: wheelHandlerFunc, useCapture @@ -658,6 +689,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: uiEventHandlerFunc, useCapture @@ -702,6 +735,8 @@ var LibraryHTML5 = { var eventHandler = { target: findEventTarget(target), eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: focusEventHandlerFunc, useCapture @@ -759,6 +794,8 @@ var LibraryHTML5 = { var eventHandler = { target: findEventTarget(target), eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: deviceOrientationEventHandlerFunc, useCapture @@ -827,6 +864,8 @@ var LibraryHTML5 = { var eventHandler = { target: findEventTarget(target), eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: deviceMotionEventHandlerFunc, useCapture @@ -907,6 +946,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: orientationChangeEventHandlerFunc, useCapture @@ -1014,6 +1055,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: fullscreenChangeEventhandlerFunc, useCapture @@ -1512,6 +1555,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: pointerlockChangeEventHandlerFunc, useCapture @@ -1556,6 +1601,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: pointerlockErrorEventHandlerFunc, useCapture @@ -1706,6 +1753,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: visibilityChangeEventHandlerFunc, useCapture @@ -1821,6 +1870,8 @@ var LibraryHTML5 = { allowsDeferredCalls: eventTypeString == 'touchstart' || eventTypeString == 'touchend', #endif eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: touchEventHandlerFunc, useCapture @@ -1904,6 +1955,8 @@ var LibraryHTML5 = { allowsDeferredCalls: true, #endif eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: gamepadEventHandlerFunc, useCapture @@ -1990,6 +2043,8 @@ var LibraryHTML5 = { var eventHandler = { target: findEventTarget(target), eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: beforeUnloadEventHandlerFunc, useCapture @@ -2040,6 +2095,8 @@ var LibraryHTML5 = { var eventHandler = { target: battery, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: batteryEventHandlerFunc, useCapture diff --git a/src/lib/libhtml5_webgl.js b/src/lib/libhtml5_webgl.js index d00be138946d8..f579ae57175af 100644 --- a/src/lib/libhtml5_webgl.js +++ b/src/lib/libhtml5_webgl.js @@ -440,6 +440,8 @@ var LibraryHtml5WebGL = { var eventHandler = { target: findEventTarget(target), eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: webGlEventHandlerFunc, useCapture diff --git a/src/lib/libsigs.js b/src/lib/libsigs.js index 6790d6fd6bcf4..72ffd62cef6f3 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -666,6 +666,7 @@ sigs = { emscripten_has_threading_support__sig: 'i', emscripten_hide_mouse__sig: 'v', emscripten_html5_remove_all_event_listeners__sig: 'v', + emscripten_html5_remove_event_listener__sig: 'ippip', emscripten_idb_async_clear__sig: 'vpppp', emscripten_idb_async_delete__sig: 'vppppp', emscripten_idb_async_exists__sig: 'vppppp', diff --git a/system/include/emscripten/html5.h b/system/include/emscripten/html5.h index 6fb863e2f77b9..34004f10549f4 100644 --- a/system/include/emscripten/html5.h +++ b/system/include/emscripten/html5.h @@ -420,6 +420,8 @@ EMSCRIPTEN_RESULT emscripten_get_element_css_size(const char *target __attribute void emscripten_html5_remove_all_event_listeners(void); +EMSCRIPTEN_RESULT emscripten_html5_remove_event_listener(const char *target __attribute__((nonnull)), void *userData, int eventTypeId, void *callback __attribute__((nonnull))); + #define EM_CALLBACK_THREAD_CONTEXT_MAIN_RUNTIME_THREAD ((pthread_t)0x1) #define EM_CALLBACK_THREAD_CONTEXT_CALLING_THREAD ((pthread_t)0x2) diff --git a/test/codesize/test_codesize_hello_dylink_all.json b/test/codesize/test_codesize_hello_dylink_all.json index 5dd7e9825e044..fad1c6b6573b9 100644 --- a/test/codesize/test_codesize_hello_dylink_all.json +++ b/test/codesize/test_codesize_hello_dylink_all.json @@ -1,7 +1,7 @@ { - "a.out.js": 245421, + "a.out.js": 245799, "a.out.nodebug.wasm": 574167, - "total": 819588, + "total": 819966, "sent": [ "IMG_Init", "IMG_Load", @@ -681,6 +681,7 @@ "emscripten_has_asyncify", "emscripten_hide_mouse", "emscripten_html5_remove_all_event_listeners", + "emscripten_html5_remove_event_listener", "emscripten_idb_async_clear", "emscripten_idb_async_delete", "emscripten_idb_async_exists", diff --git a/test/test_browser.py b/test/test_browser.py index 43ca990a7d1f1..b5bcf9296141c 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -2675,6 +2675,9 @@ def test_html5_core(self, opts): self.cflags.append('--pre-js=pre.js') self.btest_exit('test_html5_core.c', cflags=opts) + def test_html5_remove_event_listener(self): + self.btest_exit('test_html5_remove_event_listener.c') + @parameterized({ '': ([],), 'closure': (['-O2', '-g1', '--closure=1'],), diff --git a/test/test_html5_remove_event_listener.c b/test/test_html5_remove_event_listener.c new file mode 100644 index 0000000000000..394e7afe31ee7 --- /dev/null +++ b/test/test_html5_remove_event_listener.c @@ -0,0 +1,134 @@ +/* + * Copyright 2025 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + */ + +#include +#include +#include +#include +#include + +const char *emscripten_result_to_string(EMSCRIPTEN_RESULT result) { + if (result == EMSCRIPTEN_RESULT_SUCCESS) return "EMSCRIPTEN_RESULT_SUCCESS"; + if (result == EMSCRIPTEN_RESULT_DEFERRED) return "EMSCRIPTEN_RESULT_DEFERRED"; + if (result == EMSCRIPTEN_RESULT_NOT_SUPPORTED) return "EMSCRIPTEN_RESULT_NOT_SUPPORTED"; + if (result == EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED) return "EMSCRIPTEN_RESULT_FAILED_NOT_DEFERRED"; + if (result == EMSCRIPTEN_RESULT_INVALID_TARGET) return "EMSCRIPTEN_RESULT_INVALID_TARGET"; + if (result == EMSCRIPTEN_RESULT_UNKNOWN_TARGET) return "EMSCRIPTEN_RESULT_UNKNOWN_TARGET"; + if (result == EMSCRIPTEN_RESULT_INVALID_PARAM) return "EMSCRIPTEN_RESULT_INVALID_PARAM"; + if (result == EMSCRIPTEN_RESULT_FAILED) return "EMSCRIPTEN_RESULT_FAILED"; + if (result == EMSCRIPTEN_RESULT_NO_DATA) return "EMSCRIPTEN_RESULT_NO_DATA"; + return "Unknown EMSCRIPTEN_RESULT!"; +} + +// Report API failure +#define TEST_RESULT(x) if (ret != EMSCRIPTEN_RESULT_SUCCESS) printf("%s returned %s.\n", #x, emscripten_result_to_string(ret)); + +// Like above above but also assert API success +#define ASSERT_RESULT(x) TEST_RESULT(x); assert(ret == EMSCRIPTEN_RESULT_SUCCESS); + +bool key_callback_1(int eventType, const EmscriptenKeyboardEvent *e, void *userData) { + printf("key_callback_1: eventType=%d, userData=%s\n", eventType, (char const *) userData); + return 0; +} + +bool key_callback_2(int eventType, const EmscriptenKeyboardEvent *e, void *userData) { + printf("key_callback_2: eventType=%d, userData=%s\n", eventType, (char const *) userData); + return 0; +} + +bool mouse_callback_1(int eventType, const EmscriptenMouseEvent *e, void *userData) { + printf("mouse_callback_1: eventType=%d, userData=%s\n", eventType, (char const *) userData); + return 0; +} + +void checkCount(int count) { + int eventHandlersCount = EM_ASM_INT({ return JSEvents.eventHandlers.length; }); + printf("Detected [%d] handlers\n", eventHandlersCount); + assert(count == eventHandlersCount); +} + +int main() { + bool useCapture = true; + void *userData3 = "3"; + + // no event listeners yet + assert(emscripten_html5_remove_event_listener(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, useCapture, key_callback_1) == EMSCRIPTEN_RESULT_INVALID_PARAM); + + checkCount(0); + + EMSCRIPTEN_RESULT ret = emscripten_set_keypress_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, useCapture, key_callback_1); + ASSERT_RESULT(emscripten_set_keypress_callback); + ret = emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, useCapture, key_callback_1); + ASSERT_RESULT(emscripten_set_keydown_callback); + ret = emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, useCapture, key_callback_1); + ASSERT_RESULT(emscripten_set_keyup_callback); + + checkCount(3); + + // removing keydown event + ret = emscripten_html5_remove_event_listener(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, EMSCRIPTEN_EVENT_KEYDOWN, key_callback_1); + ASSERT_RESULT(emscripten_html5_remove_event_listener); + + checkCount(2); + + // adding another keypress callback on the same target + ret = emscripten_set_keypress_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, useCapture, key_callback_2); + ASSERT_RESULT(emscripten_set_keypress_callback); + + checkCount(3); + + // adding another keypress callback on the same target with different user data + ret = emscripten_set_keypress_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, userData3, useCapture, key_callback_2); + ASSERT_RESULT(emscripten_set_keypress_callback); + + checkCount(4); + + // checking invalid argument: wrong target + assert(emscripten_html5_remove_event_listener("this_dom_element_does_not_exist", userData3, EMSCRIPTEN_EVENT_KEYPRESS, key_callback_2) == EMSCRIPTEN_RESULT_INVALID_PARAM); + // checking invalid argument: wrong userData + assert(emscripten_html5_remove_event_listener(EMSCRIPTEN_EVENT_TARGET_WINDOW, "abc", EMSCRIPTEN_EVENT_KEYPRESS, key_callback_2) == EMSCRIPTEN_RESULT_INVALID_PARAM); + // checking invalid argument: wrong eventTypeId + assert(emscripten_html5_remove_event_listener(EMSCRIPTEN_EVENT_TARGET_WINDOW, userData3, EMSCRIPTEN_EVENT_BLUR, key_callback_2) == EMSCRIPTEN_RESULT_INVALID_PARAM); + // checking invalid argument: wrong callback + assert(emscripten_html5_remove_event_listener(EMSCRIPTEN_EVENT_TARGET_WINDOW, userData3, EMSCRIPTEN_EVENT_KEYPRESS, mouse_callback_1) == EMSCRIPTEN_RESULT_INVALID_PARAM); + + checkCount(4); + + // removing keypress / userData=NULL / key_callback_2 + ret = emscripten_html5_remove_event_listener(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, EMSCRIPTEN_EVENT_KEYPRESS, key_callback_2); + ASSERT_RESULT(emscripten_html5_remove_event_listener); + + checkCount(3); + + // removing keypress / userData=NULL / key_callback_1 + ret = emscripten_html5_remove_event_listener(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, EMSCRIPTEN_EVENT_KEYPRESS, key_callback_1); + ASSERT_RESULT(emscripten_html5_remove_event_listener); + + checkCount(2); + + // removing keypress / userData=3 / key_callback_2 + ret = emscripten_html5_remove_event_listener(EMSCRIPTEN_EVENT_TARGET_WINDOW, userData3, EMSCRIPTEN_EVENT_KEYPRESS, key_callback_2); + ASSERT_RESULT(emscripten_html5_remove_event_listener); + + checkCount(1); + + // adding the same mouse down callback to 2 different targets + ret = emscripten_set_mousedown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, useCapture, mouse_callback_1); + ASSERT_RESULT(emscripten_set_mousedown_callback); + ret = emscripten_set_mousedown_callback("#canvas", NULL, useCapture, mouse_callback_1); + ASSERT_RESULT(emscripten_set_mousedown_callback); + + checkCount(3); + + // removing mousedown / userData=NULL / mouse_callback_1 on the window target + ret = emscripten_html5_remove_event_listener(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, EMSCRIPTEN_EVENT_MOUSEDOWN, mouse_callback_1); + ASSERT_RESULT(emscripten_html5_remove_event_listener); + + checkCount(2); + + return 0; +}