diff --git a/src/lib/libhtml5.js b/src/lib/libhtml5.js index b7363179d097b..119a9ed0e70b3 100644 --- a/src/lib/libhtml5.js +++ b/src/lib/libhtml5.js @@ -205,6 +205,25 @@ var LibraryHTML5 = { return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}}; }, + removeSingleHandler(eventHandler) { + if (!eventHandler.target) { +#if ASSERTIONS + err('removeSingleHandler: the target element for event handler registration does not exist, when processing the following event handler registration:'); + console.dir(eventHandler); +#endif + return {{{ cDefs.EMSCRIPTEN_RESULT_UNKNOWN_TARGET }}}; + } + for (var i = 0; i < JSEvents.eventHandlers.length; ++i) { + if (JSEvents.eventHandlers[i].target === eventHandler.target + && JSEvents.eventHandlers[i].eventTypeId === eventHandler.eventTypeId + && JSEvents.eventHandlers[i].callbackfunc === eventHandler.callbackfunc + && JSEvents.eventHandlers[i].userData === eventHandler.userData) { + JSEvents._removeHandler(i--); + } + } + return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}}; + }, + #if PTHREADS getTargetThreadForEventCallback(targetThread) { switch (targetThread) { @@ -298,6 +317,8 @@ var LibraryHTML5 = { var eventHandler = { target: findEventTarget(target), eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: keyEventHandlerFunc, useCapture @@ -412,6 +433,18 @@ var LibraryHTML5 = { }, #endif + emscripten_remove_callback__proxy: 'sync', + emscripten_remove_callback__deps: ['$JSEvents', '$findEventTarget'], + emscripten_remove_callback: (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) => @@ -503,6 +536,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 @@ -599,6 +634,8 @@ var LibraryHTML5 = { allowsDeferredCalls: true, #endif eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: wheelHandlerFunc, useCapture @@ -674,6 +711,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: uiEventHandlerFunc, useCapture @@ -721,6 +760,8 @@ var LibraryHTML5 = { var eventHandler = { target: findEventTarget(target), eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: focusEventHandlerFunc, useCapture @@ -779,6 +820,8 @@ var LibraryHTML5 = { var eventHandler = { target: findEventTarget(target), eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: deviceOrientationEventHandlerFunc, useCapture @@ -849,6 +892,8 @@ var LibraryHTML5 = { var eventHandler = { target: findEventTarget(target), eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: deviceMotionEventHandlerFunc, useCapture @@ -935,6 +980,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: orientationChangeEventHandlerFunc, useCapture @@ -1046,6 +1093,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: fullscreenChangeEventhandlerFunc, useCapture @@ -1547,6 +1596,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: pointerlockChangeEventHandlerFunc, useCapture @@ -1591,6 +1642,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: pointerlockErrorEventHandlerFunc, useCapture @@ -1745,6 +1798,8 @@ var LibraryHTML5 = { var eventHandler = { target, eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: visibilityChangeEventHandlerFunc, useCapture @@ -1863,6 +1918,8 @@ var LibraryHTML5 = { allowsDeferredCalls: eventTypeString == 'touchstart' || eventTypeString == 'touchend', #endif eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: touchEventHandlerFunc, useCapture @@ -1949,6 +2006,8 @@ var LibraryHTML5 = { allowsDeferredCalls: true, #endif eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: gamepadEventHandlerFunc, useCapture @@ -2035,6 +2094,8 @@ var LibraryHTML5 = { var eventHandler = { target: findEventTarget(target), eventTypeString, + eventTypeId, + userData, callbackfunc, handlerFunc: beforeUnloadEventHandlerFunc, useCapture @@ -2088,6 +2149,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 63f8d2ad39624..ed8b4bf27577c 100644 --- a/src/lib/libhtml5_webgl.js +++ b/src/lib/libhtml5_webgl.js @@ -441,6 +441,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 04e36d080ee3f..3078715a905e4 100644 --- a/src/lib/libsigs.js +++ b/src/lib/libsigs.js @@ -731,6 +731,7 @@ sigs = { emscripten_promise_resolve__sig: 'vpip', emscripten_promise_then__sig: 'ppppp', emscripten_random__sig: 'f', + emscripten_remove_callback__sig: 'ippip', emscripten_request_animation_frame__sig: 'ipp', emscripten_request_animation_frame_loop__sig: 'vpp', emscripten_request_fullscreen__sig: 'ipi', diff --git a/system/include/emscripten/html5.h b/system/include/emscripten/html5.h index 6fb863e2f77b9..667a0a9c6d1d4 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_remove_callback(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/test_browser.py b/test/test_browser.py index a9f9b71d86be2..6bdaa8facbf1f 100644 --- a/test/test_browser.py +++ b/test/test_browser.py @@ -2634,6 +2634,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_callback(self): + self.btest_exit('test_html5_remove_callback.c') + @parameterized({ '': ([],), 'closure': (['-O2', '-g1', '--closure=1'],), diff --git a/test/test_html5_remove_callback.c b/test/test_html5_remove_callback.c new file mode 100644 index 0000000000000..b583775f89e39 --- /dev/null +++ b/test/test_html5_remove_callback.c @@ -0,0 +1,136 @@ +/* + * 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); + +char const *userDataToString(void *userData) { + return userData ? (char const *) userData : "nullptr"; +} + +bool key_callback_1(int eventType, const EmscriptenKeyboardEvent *e, void *userData) { + printf("key_callback_1: eventType=%d, userData=%s\n", eventType, userDataToString(userData)); + return 0; +} + +bool key_callback_2(int eventType, const EmscriptenKeyboardEvent *e, void *userData) { + printf("key_callback_2: eventType=%d, userData=%s\n", eventType, userDataToString(userData)); + return 0; +} + +bool mouse_callback_1(int eventType, const EmscriptenMouseEvent *e, void *userData) { + printf("mouse_callback_1: eventType=%d, userData=%s\n", eventType, userDataToString(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); +} + +void test_done() {} + +int main() { + bool useCapture = true; + void *userData3 = "3"; + + // first we check for invalid parameters + assert(emscripten_remove_callback("this_dom_element_does_not_exist", NULL, 0, key_callback_1) == EMSCRIPTEN_RESULT_UNKNOWN_TARGET); + + 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_remove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, EMSCRIPTEN_EVENT_KEYDOWN, key_callback_1); + ASSERT_RESULT(emscripten_remove_callback); + + 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); + + // removing a combination that does not exist (no mouse_callback_1 registered) + ret = emscripten_remove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, EMSCRIPTEN_EVENT_KEYPRESS, mouse_callback_1); + ASSERT_RESULT(emscripten_remove_callback); + + checkCount(4); + + // removing keypress / userData=NULL / key_callback_2 + ret = emscripten_remove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, EMSCRIPTEN_EVENT_KEYPRESS, key_callback_2); + ASSERT_RESULT(emscripten_remove_callback); + + checkCount(3); + + // removing keypress / userData=NULL / key_callback_1 + ret = emscripten_remove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, EMSCRIPTEN_EVENT_KEYPRESS, key_callback_1); + ASSERT_RESULT(emscripten_remove_callback); + + checkCount(2); + + // removing keypress / userData=3 / key_callback_2 + ret = emscripten_remove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, userData3, EMSCRIPTEN_EVENT_KEYPRESS, key_callback_2); + ASSERT_RESULT(emscripten_remove_callback); + + 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_remove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, EMSCRIPTEN_EVENT_MOUSEDOWN, mouse_callback_1); + ASSERT_RESULT(emscripten_remove_callback); + + checkCount(2); + + return 0; +}