-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Add a way to remove a single event listener (#20983) #25535
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
adeb50b
4260b0b
6925d65
d489b58
9fc8bd1
55d37cf
b7b7240
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really love all the repetition here, or the fact that we need to store two extra fields just for this new API, but I guess it makes sense, and the repetitive nature of this file is kind of a pre-existing condition :)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. I don't love it either, but you are right, this code is replicated over and over (17 times!). I feel like the API could have been simplified in the first place (having just 1 function call for all the calls using the same callback type and providing the eventTypeId... which would have reduced, for example, 9 mouse related calls into just 1...). It is what is now unfortunately. So that is the only solution I can think of to be able to implement this new API.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me correct what I just said in regards to the count. There are indeed 17 places where I had to change the code, but there are way many more "emscripten_set_xxx_callback". There are 17 types of events supported, so we could not really simplyif this since they all have different callback (function pointer) types. The example I gave for mouse counts as just 1 on these 17 places because there are 9 "emscripten_set_mousexxx_callback" functions which all funnel into 1
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed the original API shape is a bit more verbose than what I would like. Pardon, this was my very first API design into Emscripten way back in 2014. I am not much worried about the repetition though, since it will closure minify to just a condensed One could create a map of ID->string to avoid carrying both ID and string here, but storing that map would take up more bytes than the added |
||
| callbackfunc, | ||
| handlerFunc: focusEventHandlerFunc, | ||
| useCapture | ||
|
|
@@ -779,6 +820,8 @@ var LibraryHTML5 = { | |
| var eventHandler = { | ||
| target: findEventTarget(target), | ||
| eventTypeString, | ||
| eventTypeId, | ||
| userData, | ||
| callbackfunc, | ||
| handlerFunc: deviceOrientationEventHandlerFunc, | ||
| useCapture | ||
|
|
@@ -850,6 +893,8 @@ var LibraryHTML5 = { | |
| var eventHandler = { | ||
| target: findEventTarget(target), | ||
| eventTypeString, | ||
| eventTypeId, | ||
| userData, | ||
| callbackfunc, | ||
| handlerFunc: deviceMotionEventHandlerFunc, | ||
| useCapture | ||
|
|
@@ -936,6 +981,8 @@ var LibraryHTML5 = { | |
| var eventHandler = { | ||
| target, | ||
| eventTypeString, | ||
| eventTypeId, | ||
| userData, | ||
| callbackfunc, | ||
| handlerFunc: orientationChangeEventHandlerFunc, | ||
| useCapture | ||
|
|
@@ -1047,6 +1094,8 @@ var LibraryHTML5 = { | |
| var eventHandler = { | ||
| target, | ||
| eventTypeString, | ||
| eventTypeId, | ||
| userData, | ||
| callbackfunc, | ||
| handlerFunc: fullscreenChangeEventhandlerFunc, | ||
| useCapture | ||
|
|
@@ -1548,6 +1597,8 @@ var LibraryHTML5 = { | |
| var eventHandler = { | ||
| target, | ||
| eventTypeString, | ||
| eventTypeId, | ||
| userData, | ||
| callbackfunc, | ||
| handlerFunc: pointerlockChangeEventHandlerFunc, | ||
| useCapture | ||
|
|
@@ -1592,6 +1643,8 @@ var LibraryHTML5 = { | |
| var eventHandler = { | ||
| target, | ||
| eventTypeString, | ||
| eventTypeId, | ||
| userData, | ||
| callbackfunc, | ||
| handlerFunc: pointerlockErrorEventHandlerFunc, | ||
| useCapture | ||
|
|
@@ -1746,6 +1799,8 @@ var LibraryHTML5 = { | |
| var eventHandler = { | ||
| target, | ||
| eventTypeString, | ||
| eventTypeId, | ||
| userData, | ||
| callbackfunc, | ||
| handlerFunc: visibilityChangeEventHandlerFunc, | ||
| useCapture | ||
|
|
@@ -1864,6 +1919,8 @@ var LibraryHTML5 = { | |
| allowsDeferredCalls: eventTypeString == 'touchstart' || eventTypeString == 'touchend', | ||
| #endif | ||
| eventTypeString, | ||
| eventTypeId, | ||
| userData, | ||
| callbackfunc, | ||
| handlerFunc: touchEventHandlerFunc, | ||
| useCapture | ||
|
|
@@ -1950,6 +2007,8 @@ var LibraryHTML5 = { | |
| allowsDeferredCalls: true, | ||
| #endif | ||
| eventTypeString, | ||
| eventTypeId, | ||
| userData, | ||
| callbackfunc, | ||
| handlerFunc: gamepadEventHandlerFunc, | ||
| useCapture | ||
|
|
@@ -2036,6 +2095,8 @@ var LibraryHTML5 = { | |
| var eventHandler = { | ||
| target: findEventTarget(target), | ||
| eventTypeString, | ||
| eventTypeId, | ||
| userData, | ||
| callbackfunc, | ||
| handlerFunc: beforeUnloadEventHandlerFunc, | ||
| useCapture | ||
|
|
@@ -2089,6 +2150,8 @@ var LibraryHTML5 = { | |
| var eventHandler = { | ||
| target: battery, | ||
| eventTypeString, | ||
| eventTypeId, | ||
| userData, | ||
| callbackfunc, | ||
| handlerFunc: batteryEventHandlerFunc, | ||
| useCapture | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <assert.h> | ||
| #include <stdio.h> | ||
| #include <emscripten.h> | ||
| #include <string.h> | ||
| #include <emscripten/html5.h> | ||
|
|
||
| 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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, should we key the
userDatapointer into the removal? I.e. it would be better for this API to beThe rationale is that the
userDatapointer is generally just data to the function call, and not identifying. I.e. it is more of the 'value' part in a key->value association.It might be some function local value in the caller (e.g. a boolean for some behavior), and might result in user having to store the
userDatafield in some global to be able to unregister. So requiring users to know to match theuserDatavalue could be a bit tedious.(If users did specifically want to differentiate unregistrations based on what was seemingly a different userData value, they could do that by separating to a different function with a trampoline)
Also, this function would be good to follow the same
_on_threadmodel, so that it can be called from pthreads symmetric to how registering on pthreads works.The semantics of function
emscripten_remove_callbackare to remove all copies of callback registrations of the given function. I wonder if the function naming should somehow reflect that?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this makes sense to me, but in that case I think it would also be good it if we make it impossible to register the same callback function for the same event with different userData.
If we didn't error in the second case, and allowed the same callback to be registred twice, there would be no way to unregister just one of them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think what @juj suggested was to indeed un-register all callbacks that were registered with different data but with the same target/event_id/callback combination. That would have eliminated having to store the user data.
It sounds like you don't like this because then "there would be no way to unregister just one of them".
I am definitely not in favor of erroring when registering 2 identical callbacks with different user data, because from a developer point of view, this is absolutely not an error and I can imagine scenarios where that could be useful.
So I guess the solution is to leave the API and changes as implemented in this PR. That way we can un-register a single callback (which IS the point of this PR). So I am personally in favor of this.
I will now work on updating the documentation and fixing the "Code Size" failure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you give an example of when this might be useful?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This a theoretical example, because I am not implementing it this way at the moment, but imagine for GLFW, you have 2 windows (represented in C by GLFWwindow pointers and canvases in HTML) and you need to set mouse move callbacks to track the mouse movements.
In order to detect moving the mouse outside the canvas while the button is clicked (something my library supports where others don't) you cannot set the callback on each canvas, it has to be set on the window... so the callbacks would be:
Again I am not doing it this way, but that could be a way to implement it...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough. If we do want to officially support that then we probably also want to officially support unregistering them individually (sadly)