From d31e880cc327c1cf523f6de0db7e57fd44a21971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Mon, 10 Aug 2026 00:32:01 -0300 Subject: [PATCH] Fix bidirectional clipboard support in Emscripten builds: SDL2 prevents the default action for Ctrl/Cmd key events on Emscripten, which stops browsers from emitting the native paste event. Intercept Ctrl/Cmd+V before SDL2 consumes it, allow the browser paste event to update the internal clipboard, and replay the keydown so the application executes its normal paste command. Also refresh the clipboard through the asynchronous Clipboard API when opening a context menu, enabling paste actions triggered without a keyboard shortcut. Export Emscripten's ccall runtime method so the browser clipboard helper can reliably invoke its C++ callbacks in optimized builds, and disable clang-format for the header containing embedded JavaScript. --- premake4.lua | 2 +- premake5.lua | 2 +- .../emscripten_browser_clipboard.h | 35 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/premake4.lua b/premake4.lua index 729fafc5f..1f7952609 100644 --- a/premake4.lua +++ b/premake4.lua @@ -659,7 +659,7 @@ function build_link_configuration( package_name, use_ee_icon ) add_cross_config_links() configuration "emscripten" - linkoptions { "-s TOTAL_MEMORY=536870912 -s ALLOW_MEMORY_GROWTH=1 -s USE_SDL=2 -s ENVIRONMENT=worker,web" } + linkoptions { "-s TOTAL_MEMORY=536870912 -s ALLOW_MEMORY_GROWTH=1 -s USE_SDL=2 -s ENVIRONMENT=worker,web -s EXPORTED_RUNTIME_METHODS=ccall" } buildoptions { "-s USE_SDL=2" } buildoptions { "-s USE_PTHREADS=1" } linkoptions { "-s USE_PTHREADS=1 -sPTHREAD_POOL_SIZE=8" } diff --git a/premake5.lua b/premake5.lua index 773be9800..1cacbc912 100644 --- a/premake5.lua +++ b/premake5.lua @@ -625,7 +625,7 @@ function build_link_configuration( package_name, use_ee_icon ) if package_name ~= "eepp" and package_name ~= "eepp-static" then targetextension ".html" end - linkoptions { "-O3 -s TOTAL_MEMORY=536870912 -s ALLOW_MEMORY_GROWTH=1 -s USE_SDL=2 -s ENVIRONMENT=worker,web" } + linkoptions { "-O3 -s TOTAL_MEMORY=536870912 -s ALLOW_MEMORY_GROWTH=1 -s USE_SDL=2 -s ENVIRONMENT=worker,web -s EXPORTED_RUNTIME_METHODS=ccall" } buildoptions { "-O3 -s USE_SDL=2" } buildoptions { "-s USE_PTHREADS=1" } linkoptions { "-s USE_PTHREADS=1 -sPTHREAD_POOL_SIZE=8" } diff --git a/src/thirdparty/emscripten-browser-clipboard/emscripten_browser_clipboard.h b/src/thirdparty/emscripten-browser-clipboard/emscripten_browser_clipboard.h index 9e77261d7..d24a028cd 100644 --- a/src/thirdparty/emscripten-browser-clipboard/emscripten_browser_clipboard.h +++ b/src/thirdparty/emscripten-browser-clipboard/emscripten_browser_clipboard.h @@ -1,6 +1,8 @@ #ifndef EMSCRIPTEN_BROWSER_CLIPBOARD_H_INCLUDED #define EMSCRIPTEN_BROWSER_CLIPBOARD_H_INCLUDED +// clang-format off + #include #include @@ -33,8 +35,39 @@ EM_JS_INLINE(void, paste_js, (paste_handler callback, void *callback_data), { /// Register the given callback to handle paste events. Callback data pointer is passed through to the callback. /// Paste handler callback signature is: /// void my_handler(std::string const &paste_data, void *callback_data = nullptr); + /// SDL2's Emscripten backend prevents the default action for every Ctrl key event, which suppresses + /// the browser paste event. Let the browser handle Ctrl/Cmd+V first, then replay the keydown after + /// the clipboard contents have reached C++ so SDL delivers the normal application paste command. + let paste_key_event = null; + window.addEventListener('keydown', (event) => { + if (event.isTrusted && !event.altKey && (event.ctrlKey || event.metaKey) && + (event.code === 'KeyV' || event.key.toLowerCase() === 'v')) { + paste_key_event = event; + event.stopPropagation(); + } + }, true); + window.addEventListener('mousedown', (event) => { + if (event.isTrusted && event.button === 2 && navigator.clipboard && navigator.clipboard.readText) { + navigator.clipboard.readText().then((text) => { + Module["ccall"]('paste_return', 'number', ['string', 'number', 'number'], [text, callback, callback_data]); + }).catch(() => {}); + } + }, true); document.addEventListener('paste', (event) => { Module["ccall"]('paste_return', 'number', ['string', 'number', 'number'], [event.clipboardData.getData('text/plain'), callback, callback_data]); + if (paste_key_event) { + paste_key_event.target.dispatchEvent(new KeyboardEvent('keydown', { + key: paste_key_event.key, + code: paste_key_event.code, + location: paste_key_event.location, + ctrlKey: paste_key_event.ctrlKey, + metaKey: paste_key_event.metaKey, + shiftKey: paste_key_event.shiftKey, + bubbles: true, + cancelable: true + })); + paste_key_event = null; + } }); }); @@ -96,4 +129,6 @@ EMSCRIPTEN_KEEPALIVE inline char const *copy_return(copy_handler callback, void } +// clang-format on + #endif // EMSCRIPTEN_BROWSER_CLIPBOARD_H_INCLUDED