From d70f7422b7e2a334228fbfddd65d73ffbfaae968 Mon Sep 17 00:00:00 2001 From: Sten Feldman Date: Tue, 13 Feb 2024 21:30:18 +0200 Subject: [PATCH] feat: Support Copy/Paste on WASM build outside the application border (#1542) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Problem description WASM build does not support copy/paste beyond the application. Meaning, there's no practical way of sending text back and forth across the application border. There are lengthy threads why this is a technical challenge in WASM/Browser world, e.g: - https://github.com/pthom/hello_imgui/issues/3 - https://github.com/emscripten-core/emscripten/pull/19510 ### Implementation description Implements a workaround solution as Header only C++ library, as proposed and implemented at: https://github.com/Armchair-Software/emscripten-browser-clipboard Maybe there are cleaner ways of achieving the functionality. Definitely would like to have some discussion around this. 👀 ℹī¸ The proposed PR "works for me" on Windows, using CTRL-C/V shortcuts to copy text from and to the application. On MacOS the system shortcut to Paste is different from what ImHex has defined. This results in system Paste shortcut of command-V triggering the browser callback to synchronise the application clipboard, but no actual Paste takes place within ImHex. If there would be a clean way to trigger the paste command, that would be wonderful (or get the context and references to write the data to the cursor, but I was unable to find a clean solution). The only proposed solutions in the referenced threads were about triggering paste event internally via Key events. This seemed wonky 🙃 , so is not currently implemented. At the moment the paste on MacOS is command+V followed by control+V. ### Additional things This is definitely a stopgap solution before the ImGui and Emscripten take a more proper solution in enabling Copy/Paste outside the application borders. However, I feel like this is a must have capability to make the WASM build more useful, not just for trying out ImHex. Cheers! đŸģ --------- Co-authored-by: Nik --- .../include/emscripten_browser_clipboard.h | 99 +++++++++++++++++++ .../imgui/custom/source/imgui_impl_glfw.cpp | 26 +++++ 2 files changed, 125 insertions(+) create mode 100644 lib/third_party/imgui/custom/include/emscripten_browser_clipboard.h diff --git a/lib/third_party/imgui/custom/include/emscripten_browser_clipboard.h b/lib/third_party/imgui/custom/include/emscripten_browser_clipboard.h new file mode 100644 index 000000000..b48a7ae31 --- /dev/null +++ b/lib/third_party/imgui/custom/include/emscripten_browser_clipboard.h @@ -0,0 +1,99 @@ +#ifndef EMSCRIPTEN_BROWSER_CLIPBOARD_H_INCLUDED +#define EMSCRIPTEN_BROWSER_CLIPBOARD_H_INCLUDED + +#include +#include + +#define _EM_JS_INLINE(ret, c_name, js_name, params, code) \ + extern "C" { \ + ret c_name params EM_IMPORT(js_name); \ + EMSCRIPTEN_KEEPALIVE \ + __attribute__((section("em_js"), aligned(1))) inline char __em_js__##js_name[] = \ + #params "<::>" code; \ + } + +#define EM_JS_INLINE(ret, name, params, ...) _EM_JS_INLINE(ret, name, name, params, #__VA_ARGS__) + +namespace emscripten_browser_clipboard { + +/////////////////////////////////// Interface ////////////////////////////////// + +using paste_handler = void(*)(std::string const&, void*); +using copy_handler = char const*(*)(void*); + +inline void paste(paste_handler callback, void *callback_data = nullptr); +inline void copy(copy_handler callback, void *callback_data = nullptr); +inline void copy(std::string const &content); + +///////////////////////////////// Implementation /////////////////////////////// + +namespace { + +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); + document.addEventListener('paste', (event) => { + Module["ccall"]('paste_return', 'number', ['string', 'number', 'number'], [event.clipboardData.getData('text/plain'), callback, callback_data]); + }); +}); + +EM_JS_INLINE(void, copy_js, (copy_handler callback, void *callback_data), { + /// Register the given callback to handle copy events. Callback data pointer is passed through to the callback. + /// Copy handler callback signature is: + /// char const *my_handler(void *callback_data = nullptr); + document.addEventListener('copy', (event) => { + const content_ptr = Module["ccall"]('copy_return', 'number', ['number', 'number'], [callback, callback_data]); + event.clipboardData.setData('text/plain', UTF8ToString(content_ptr)); + event.preventDefault(); + }); +}); + +EM_JS_INLINE(void, copy_async_js, (char const *content_ptr), { + /// Attempt to copy the provided text asynchronously + navigator.clipboard.writeText(UTF8ToString(content_ptr)); +}); + +} + +inline void paste(paste_handler callback, void *callback_data) { + /// C++ wrapper for javascript paste call + paste_js(callback, callback_data); +} + +inline void copy(copy_handler callback, void *callback_data) { + /// C++ wrapper for javascript copy call + copy_js(callback, callback_data); +} + +inline void copy(std::string const &content) { + /// C++ wrapper for javascript copy call + copy_async_js(content.c_str()); +} + +namespace { + +extern "C" { + +EMSCRIPTEN_KEEPALIVE inline int paste_return(char const *paste_data, paste_handler callback, void *callback_data); + +EMSCRIPTEN_KEEPALIVE inline int paste_return(char const *paste_data, paste_handler callback, void *callback_data) { + /// Call paste callback - this function is called from javascript when the paste event occurs + callback(paste_data, callback_data); + return 1; +} + +EMSCRIPTEN_KEEPALIVE inline char const *copy_return(copy_handler callback, void *callback_data); + +EMSCRIPTEN_KEEPALIVE inline char const *copy_return(copy_handler callback, void *callback_data) { + /// Call paste callback - this function is called from javascript when the paste event occurs + return callback(callback_data); +} + +} + +} + +} + +#endif // EMSCRIPTEN_BROWSER_CLIPBOARD_H_INCLUDED \ No newline at end of file diff --git a/lib/third_party/imgui/custom/source/imgui_impl_glfw.cpp b/lib/third_party/imgui/custom/source/imgui_impl_glfw.cpp index fc606668c..35e367511 100644 --- a/lib/third_party/imgui/custom/source/imgui_impl_glfw.cpp +++ b/lib/third_party/imgui/custom/source/imgui_impl_glfw.cpp @@ -107,6 +107,11 @@ #ifdef __EMSCRIPTEN__ #include #include +// IMHEX PATCH BEGIN +#include + +static std::string clipboardContent; +// IMHEX PATCH END #endif // We gather version tests as define in order to easily see which features are version-dependent. @@ -198,12 +203,24 @@ static void ImGui_ImplGlfw_ShutdownPlatformInterface(); // Functions static const char* ImGui_ImplGlfw_GetClipboardText(void* user_data) { +// IMHEX PATCH BEGIN +#ifdef __EMSCRIPTEN__ + return clipboardContent.c_str(); +#else return glfwGetClipboardString((GLFWwindow*)user_data); +#endif +// IMHEX PATCH END } static void ImGui_ImplGlfw_SetClipboardText(void* user_data, const char* text) { glfwSetClipboardString((GLFWwindow*)user_data, text); +// IMHEX PATCH BEGIN +#ifdef __EMSCRIPTEN__ + clipboardContent = text; + emscripten_browser_clipboard::copy(clipboardContent); +#endif +// IMHEX PATCH END } static ImGuiKey ImGui_ImplGlfw_KeyToImGuiKey(int key) @@ -594,6 +611,15 @@ static bool ImGui_ImplGlfw_Init(GLFWwindow* window, bool install_callbacks, Glfw bd->Time = 0.0; bd->WantUpdateMonitors = true; +// IMHEX PATCH BEGIN +#ifdef __EMSCRIPTEN__ + // Callback to handle clipboard paste from browser + emscripten_browser_clipboard::paste([](std::string const &paste_data, void *callback_data [[maybe_unused]]) { + clipboardContent = std::move(paste_data); + }); +#endif +// IMHEX PATCH END + io.SetClipboardTextFn = ImGui_ImplGlfw_SetClipboardText; io.GetClipboardTextFn = ImGui_ImplGlfw_GetClipboardText; io.ClipboardUserData = bd->Window;