diff --git a/backends/imgui_impl_emscripten.cpp b/backends/imgui_impl_emscripten.cpp new file mode 100644 index 000000000..e1cfddbaf --- /dev/null +++ b/backends/imgui_impl_emscripten.cpp @@ -0,0 +1,619 @@ +// dear imgui: Platform Backend for Emscripten +// This needs to be used along with a Renderer (e.g. OpenGL3) + +// Implemented features: +// [X] Platform: Clipboard support (with IMGUI_IMPL_EMSCRIPTEN_ENABLE_CLIPBOARD). +// [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen (with IMGUI_IMPL_EMSCRIPTEN_POINTER_EVENTS). +// [X] Platform: Keyboard support. +// [X] Platform: Gamepad support. +// [X] Platform: Mouse cursor shape and visibility (ImGuiBackendFlags_HasMouseCursors). Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. + +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp + +// Configuration flags to add in your imconfig file: +// #define IMGUI_IMPL_EMSCRIPTEN_ENABLE_CLIPBOARD // Enable clipboard support, requires "Module['_ImGui_ImplEmscripten_ClipboardPasteCallback']" +// #define IMGUI_IMPL_EMSCRIPTEN_POINTER_EVENTS // Handle inputs using pointer events, requires "Module['_ImGui_ImplEmscripten_PointerEvent']" and "touch-action: none" to properly accept touch inputs. + +#include "imgui.h" +#ifndef IMGUI_DISABLE + +#include + +struct ImGui_ImplEmscripten_BackendData +{ + const char* ClipboardTextData; + char* TargetId; + double Time; + int Width; + int Height; + ImGuiMouseCursor LastMouseCursor; + int GamepadsCount; +}; + +// Backend data stored in io.BackendPlatformUserData to allow support for multiple Dear ImGui contexts +// It is STRONGLY preferred that you use docking branch with multi-viewports (== single Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts. +static ImGui_ImplEmscripten_BackendData* ImGui_ImplEmscripten_GetBackendData() +{ + return ImGui::GetCurrentContext() ? (ImGui_ImplEmscripten_BackendData*)ImGui::GetIO().BackendPlatformUserData : nullptr; +} +static ImGui_ImplEmscripten_BackendData* ImGui_ImplEmscripten_GetBackendData(ImGuiIO& io) +{ + return (ImGui_ImplEmscripten_BackendData*)io.BackendPlatformUserData; +} + +// Functions +static bool ImGui_ImplEmscripten_UpdateMouseCursor(ImGuiIO& io, ImGuiMouseCursor imgui_cursor) +{ + if (io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange) + return false; + + ImGui_ImplEmscripten_BackendData* bd = ImGui_ImplEmscripten_GetBackendData(io); + + const char* name = NULL; + if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor) + { + name = "none"; + } + else + { + switch (imgui_cursor) + { + case ImGuiMouseCursor_Arrow: name = "default"; break; + case ImGuiMouseCursor_TextInput: name = "text"; break; + case ImGuiMouseCursor_ResizeAll: name = "move"; break; + case ImGuiMouseCursor_ResizeNS: name = "ns-resize"; break; + case ImGuiMouseCursor_ResizeEW: name = "ew-resize"; break; + case ImGuiMouseCursor_ResizeNESW: name = "nesw-resize"; break; + case ImGuiMouseCursor_ResizeNWSE: name = "nwse-resize"; break; + case ImGuiMouseCursor_Hand: name = "pointer"; break; + case ImGuiMouseCursor_Wait: name = "wait"; break; + case ImGuiMouseCursor_Progress: name = "progress"; break; + case ImGuiMouseCursor_NotAllowed: name = "not-allowed"; break; + } + } + + if (name) + { + EM_ASM({ + document.querySelector(UTF8ToString($0)).style.cursor = UTF8ToString($1); + }, bd->TargetId, name); + + return true; + } + + return false; +} + +// FIXME: Lookup complex keys in the hash map +// Not using charCode/keyCode because they're deprecated, tho SDL still uses them. +static ImGuiKey ImGui_ImplEmscripten_KeyToImGuiKey(const EM_UTF8(&key)[32]) +{ + if (key[1] == '\0') + { + char c = key[0]; + if (c >= 'a' && c <= 'z') + return (ImGuiKey)((c - 'a') + ImGuiKey_A); + else if (c >= '0' && c <= '9') + return (ImGuiKey)((c - '0') + ImGuiKey_0); + else + { + switch (c) + { + case '\'': return ImGuiKey_Apostrophe; + case ',': return ImGuiKey_Comma; + case '-': return ImGuiKey_Minus; + case '.': return ImGuiKey_Period; + case '/': return ImGuiKey_Slash; + case ';': return ImGuiKey_Semicolon; + case '=': return ImGuiKey_Equal; + case '[': return ImGuiKey_LeftBracket; + case '\\': return ImGuiKey_Backslash; + case ']': return ImGuiKey_RightBracket; + case '`': return ImGuiKey_GraveAccent; + case ' ': return ImGuiKey_Space; + case 0x09: return ImGuiKey_Tab; + case '+': return ImGuiKey_KeypadAdd; + case '*': return ImGuiKey_KeypadMultiply; + } + } + } + else + { + char c0 = key[0]; + char c1 = key[1]; + char c2 = key[2]; + if (c0 == 'F' && c1 != '\0' && (c2 == '\0' || key[3] == '\0')) + { + // F1-F9 + if (c2 == '\0' && (c1 >= '1' && c1 <= '9')) + return (ImGuiKey)((c1 - '1') + ImGuiKey_F1); + else if (c1 == '1' && (c2 >= '0' && c2 <= '9')) // F10-F19 + return (ImGuiKey)((c2 - '0') + ImGuiKey_F10); + else if (c1 == '2' && (c2 >= '0' && c2 <= '4')) // F20-F24 + return (ImGuiKey)((c2 - '0') + ImGuiKey_F20); + } + + // Slowest path + if (!strcmp(key, "Tab")) + return ImGuiKey_Tab; + else if (!strcmp(key, "ArrowLeft")) + return ImGuiKey_LeftArrow; + else if (!strcmp(key, "ArrowRight")) + return ImGuiKey_RightArrow; + else if (!strcmp(key, "ArrowUp")) + return ImGuiKey_UpArrow; + else if (!strcmp(key, "ArrowDown")) + return ImGuiKey_DownArrow; + else if (!strcmp(key, "PageUp")) + return ImGuiKey_PageUp; + else if (!strcmp(key, "PageDown")) + return ImGuiKey_PageDown; + else if (!strcmp(key, "Home")) + return ImGuiKey_Home; + else if (!strcmp(key, "End")) + return ImGuiKey_End; + else if (!strcmp(key, "Insert")) + return ImGuiKey_Insert; + else if (!strcmp(key, "Delete")) + return ImGuiKey_Delete; + else if (!strcmp(key, "Backspace")) + return ImGuiKey_Backspace; + else if (!strcmp(key, "Space")) + return ImGuiKey_Space; + else if (!strcmp(key, "Enter")) + return ImGuiKey_Enter; + else if (!strcmp(key, "Escape")) + return ImGuiKey_Escape; + else if (!strcmp(key, "ContextMenu")) + return ImGuiKey_Menu; + else if (!strcmp(key, "CapsLock")) + return ImGuiKey_CapsLock; + else if (!strcmp(key, "ScrollLock")) + return ImGuiKey_ScrollLock; + else if (!strcmp(key, "NumLock")) + return ImGuiKey_NumLock; + else if (!strcmp(key, "PrintScreen")) + return ImGuiKey_PrintScreen; + else if (!strcmp(key, "Pause")) + return ImGuiKey_Pause; + } + + return ImGuiKey_None; +} + +static bool ImGui_ImplEmscripten_KeyCallback(int type, const EmscriptenKeyboardEvent* event, void* user_data) +{ + ImGuiIO& io = ImGui::GetIO(); + + switch (type) + { + case EMSCRIPTEN_EVENT_KEYDOWN: + case EMSCRIPTEN_EVENT_KEYUP: + { + if (event->repeat) + { + if (event->key[1] == '\0') + io.AddInputCharacter(event->key[0]); + return false; + } + + io.AddKeyEvent(ImGuiMod_Ctrl, event->ctrlKey); + io.AddKeyEvent(ImGuiMod_Shift, event->shiftKey); + io.AddKeyEvent(ImGuiMod_Alt, event->altKey); + io.AddKeyEvent(ImGuiMod_Super, event->metaKey); + + ImGuiKey key = ImGui_ImplEmscripten_KeyToImGuiKey(event->key); + if (key != ImGuiKey_None) + { + bool down = type == EMSCRIPTEN_EVENT_KEYDOWN; + if (down && event->key[1] == '\0') + io.AddInputCharacter(event->key[0]); + io.AddKeyEvent(key, down); + } + + // Without this those keys won't be dispatched with "keyup" + if (key == ImGuiKey_Tab || key == ImGuiKey_Backspace) + return true; + + return false; + } + } + + return false; +} + +#ifndef IMGUI_IMPL_EMSCRIPTEN_POINTER_EVENTS +static bool ImGui_ImplEmscripten_MouseCallback(int type, const EmscriptenMouseEvent* event, void* user_data) +{ + ImGuiIO& io = ImGui::GetIO(); + + switch (type) + { + case EMSCRIPTEN_EVENT_MOUSEDOWN: + case EMSCRIPTEN_EVENT_MOUSEUP: + { + unsigned short button = event->button; + switch (button) + { + case 0: button = 0; break; + case 1: button = 2; break; + case 2: button = 1; break; + default: return false; + } + + io.AddMouseSourceEvent(ImGuiMouseSource_Mouse); + io.AddMouseButtonEvent(button, type == EMSCRIPTEN_EVENT_MOUSEDOWN); + return false; + } + case EMSCRIPTEN_EVENT_MOUSEMOVE: + { + io.AddMouseSourceEvent(ImGuiMouseSource_Mouse); + io.AddMousePosEvent((float)event->clientX, (float)event->clientY); + break; + } + } + return false; +} +#endif + +static bool ImGui_ImplEmscripten_WheelCallback(int type, const EmscriptenWheelEvent* event, void* user_data) +{ + if (type == EMSCRIPTEN_EVENT_WHEEL) + { + ImGuiIO& io = ImGui::GetIO(); + + switch (event->deltaMode) + { + case DOM_DELTA_PIXEL: + { + io.AddMouseWheelEvent(-(float)event->deltaX / 100.f, -(float)event->deltaY / 100.f); + break; + } + } + } + + return false; +} + +static bool ImGui_ImplEmscripten_FocusCallback(int type, const EmscriptenFocusEvent* event, void* user_data) +{ + ImGuiIO& io = ImGui::GetIO(); + + switch (type) + { + case EMSCRIPTEN_EVENT_FOCUSIN: + case EMSCRIPTEN_EVENT_FOCUSOUT: + { + io.AddFocusEvent(type == EMSCRIPTEN_EVENT_FOCUSIN); + break; + } + } + + return false; +} + +#include + +#ifdef IMGUI_IMPL_EMSCRIPTEN_ENABLE_CLIPBOARD +static const char* ImGui_ImplEmscripten_GetClipboardText(ImGuiContext*) +{ + ImGui_ImplEmscripten_BackendData* bd = ImGui_ImplEmscripten_GetBackendData(); + return bd->ClipboardTextData; +} + +static void ImGui_ImplEmscripten_SetClipboardText(ImGuiContext*, const char* text) +{ + ImGui_ImplEmscripten_BackendData* bd = ImGui_ImplEmscripten_GetBackendData(); + if (bd->ClipboardTextData) + free((void*)bd->ClipboardTextData); + bd->ClipboardTextData = strdup(text); + + if (bd->ClipboardTextData) + { + EM_ASM({ + try { navigator.clipboard.writeText(UTF8ToString($0)); } + catch (err) { console.error(err); } + }, bd->ClipboardTextData); + } +} + +extern "C" +{ + +EMSCRIPTEN_KEEPALIVE +void ImGui_ImplEmscripten_ClipboardPasteCallback(const char* text) +{ + ImGui_ImplEmscripten_BackendData* bd = ImGui_ImplEmscripten_GetBackendData(); + if (bd->ClipboardTextData) + free((void*)bd->ClipboardTextData); + bd->ClipboardTextData = strdup(text); +} + +}; + +static void ImGui_ImplEmscripten_SetupClipboardCallbacks(ImGui_ImplEmscripten_BackendData* bd) +{ + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + platform_io.Platform_SetClipboardTextFn = ImGui_ImplEmscripten_SetClipboardText; + platform_io.Platform_GetClipboardTextFn = ImGui_ImplEmscripten_GetClipboardText; + + EM_ASM({ + const el = document.querySelector(UTF8ToString($0)); + + document.addEventListener('paste', (ev) => { + Module['_ImGui_ImplEmscripten_ClipboardPasteCallback'](stringToNewUTF8(ev.clipboardData.getData('text/plain'))); + }); + }, bd->TargetId); +} +#endif + +#ifdef IMGUI_IMPL_EMSCRIPTEN_POINTER_EVENTS +extern "C" +{ + +#define IMGUI_IMPL_EMSCRIPTEN_POINTER_EVENT_HAS_POS 1 +#define IMGUI_IMPL_EMSCRIPTEN_POINTER_EVENT_HAS_BUTTON 2 +#define IMGUI_IMPL_EMSCRIPTEN_POINTER_EVENT_BUTTON_LEFT 4 +#define IMGUI_IMPL_EMSCRIPTEN_POINTER_EVENT_BUTTON_RIGHT 8 +#define IMGUI_IMPL_EMSCRIPTEN_POINTER_EVENT_BUTTON_MIDDLE 16 +#define IMGUI_IMPL_EMSCRIPTEN_POINTER_EVENT_BUTTON_MASK 28 + +EMSCRIPTEN_KEEPALIVE +void ImGui_ImplEmscripten_PointerEvent(int type, int pointer_type, int flags, int x, int y, bool down) +{ + ImGuiIO& io = ImGui::GetIO(); + + if (pointer_type >= 0 && pointer_type <= 2 && flags) + { + ImGuiMouseSource source; + switch (pointer_type) + { + case 0: source = ImGuiMouseSource_Mouse; break; + case 1: source = ImGuiMouseSource_TouchScreen; break; + case 2: source = ImGuiMouseSource_Pen; break; + } + + io.AddMouseSourceEvent(source); + if (flags & IMGUI_IMPL_EMSCRIPTEN_POINTER_EVENT_HAS_POS) + io.AddMousePosEvent((float)x, (float)y); + if (flags & IMGUI_IMPL_EMSCRIPTEN_POINTER_EVENT_HAS_BUTTON) + io.AddMouseButtonEvent(ImGuiMouseButton_Left, down); + } +} + +}; + +static void ImGui_ImplEmscripten_SetupPointerEvents(ImGui_ImplEmscripten_BackendData* bd) +{ + EM_ASM({ + const t = (name) => { + switch (name) { + case "mouse": return 0; + case "touch": return 1; + case "pen": return 2; + default: return -1; + } + }; + + const el = document.querySelector(UTF8ToString($0)); + const cb = Module['_ImGui_ImplEmscripten_PointerEvent']; + el.imguiHandlePointerDown = function(ev) { cb(0, t(ev.pointerType), 1 | 2, ev.clientX, ev.clientY, true) }; + el.imguiHandlePointerUp = function(ev) { cb(1, t(ev.pointerType), 1 | 2, ev.clientX, ev.clientY, false) }; + el.imguiHandlePointerMove = function(ev) { cb(2, t(ev.pointerType), 1, ev.clientX, ev.clientY, false) }; + el.imguiHandlePointerCancel = function(ev) { cb(3, t(ev.pointerType), 2, 0, 0, false) }; + el.imguiHandlePointerEnter = function(ev) { cb(4, t(ev.pointerType), 0, 0, 0, false) }; + el.imguiHandlePointerLeave = function(ev) { cb(5, t(ev.pointerType), 0, 0, 0, false) }; + + el.addEventListener('pointerdown', el.imguiHandlePointerDown); + el.addEventListener('pointerup', el.imguiHandlePointerUp); + el.addEventListener('pointermove', el.imguiHandlePointerMove); + el.addEventListener('pointercancel', el.imguiHandlePointerCancel); + el.addEventListener('pointerenter', el.imguiHandlePointerEnter); + el.addEventListener('pointerleave', el.imguiHandlePointerLeave); + }, bd->TargetId); +} + +static void ImGui_ImplEmscripten_ClearPointerEvents(ImGui_ImplEmscripten_BackendData* bd) +{ + EM_ASM({ + const el = document.querySelector(UTF8ToString($0)); + el.removeEventListener('pointerdown', el.imguiHandlePointerDown); + el.removeEventListener('pointerup', el.imguiHandlePointerUp); + el.removeEventListener('pointermove', el.imguiHandlePointerMove); + el.removeEventListener('pointercancel', el.imguiHandlePointerCancel); + el.removeEventListener('pointerenter', el.imguiHandlePointerEnter); + el.removeEventListener('pointerleave', el.imguiHandlePointerLeave); + + delete el.imguiHandlePointerDown; + delete el.imguiHandlePointerUp; + delete el.imguiHandlePointerMove; + delete el.imguiHandlePointerCancel; + delete el.imguiHandlePointerEnter; + delete el.imguiHandlePointerLeave; + }, bd->TargetId); +} +#endif + +static bool ImGui_ImplEmscripten_GamepadCallback(int type, const EmscriptenGamepadEvent* event, void* user_data) +{ + ImGuiIO& io = ImGui::GetIO(); + ImGui_ImplEmscripten_BackendData* bd = ImGui_ImplEmscripten_GetBackendData(io); + + if (type == EMSCRIPTEN_EVENT_GAMEPADCONNECTED) + bd->GamepadsCount++; + else if (type == EMSCRIPTEN_EVENT_GAMEPADDISCONNECTED) + bd->GamepadsCount--; + + if (bd->GamepadsCount > 0) + io.BackendFlags |= ImGuiBackendFlags_HasGamepad; + else + io.BackendFlags &= ~ImGuiBackendFlags_HasGamepad; + return false; +} + +static void ImGui_ImplEmscripten_UpdateGamepads(ImGuiIO& io) +{ + if (!(io.BackendFlags & ImGuiBackendFlags_HasGamepad)) + return; + + EmscriptenGamepadEvent state; + if (emscripten_sample_gamepad_data() != EMSCRIPTEN_RESULT_SUCCESS || emscripten_get_gamepad_status(0, &state) != EMSCRIPTEN_RESULT_SUCCESS) + return; + + #define IM_SATURATE(V) (V < 0.0f ? 0.0f : V > 1.0f ? 1.0f : V) + #define MAP_BUTTON(KEY_NO, BUTTON_IDX) { io.AddKeyEvent(KEY_NO, state.digitalButton[BUTTON_IDX]); } + #define MAP_ANALOG(KEY_NO, VALUE, V0, V1) { float vn = (float)(VALUE - V0) / (float)(V1 - V0); io.AddKeyAnalogEvent(KEY_NO, vn > 0.10f, IM_SATURATE(vn)); } + MAP_BUTTON(ImGuiKey_GamepadStart, 9); + MAP_BUTTON(ImGuiKey_GamepadBack, 8); + MAP_BUTTON(ImGuiKey_GamepadFaceLeft, 2); + MAP_BUTTON(ImGuiKey_GamepadFaceRight, 1); + MAP_BUTTON(ImGuiKey_GamepadFaceUp, 3); + MAP_BUTTON(ImGuiKey_GamepadFaceDown, 0); + MAP_BUTTON(ImGuiKey_GamepadDpadLeft, 14); + MAP_BUTTON(ImGuiKey_GamepadDpadRight, 15); + MAP_BUTTON(ImGuiKey_GamepadDpadUp, 12); + MAP_BUTTON(ImGuiKey_GamepadDpadDown, 13); + MAP_BUTTON(ImGuiKey_GamepadL1, 4); + MAP_BUTTON(ImGuiKey_GamepadR1, 5); + MAP_ANALOG(ImGuiKey_GamepadL2, state.analogButton[6], 0.125, 1.0); + MAP_ANALOG(ImGuiKey_GamepadR2, state.analogButton[7], 0.125, 1.0); + MAP_BUTTON(ImGuiKey_GamepadL3, 10); + MAP_BUTTON(ImGuiKey_GamepadR3, 11); + MAP_ANALOG(ImGuiKey_GamepadLStickLeft, state.axis[0], -0.25, -1.0); + MAP_ANALOG(ImGuiKey_GamepadLStickRight, state.axis[0], +0.25, +1.0); + MAP_ANALOG(ImGuiKey_GamepadLStickUp, state.axis[1], -0.25, -1.0); + MAP_ANALOG(ImGuiKey_GamepadLStickDown, state.axis[1], +0.25, +1.0); + MAP_ANALOG(ImGuiKey_GamepadRStickLeft, state.axis[2], -0.25, -1.0); + MAP_ANALOG(ImGuiKey_GamepadRStickRight, state.axis[2], +0.25, +1.0); + MAP_ANALOG(ImGuiKey_GamepadRStickUp, state.axis[3], -0.25, -1.0); + MAP_ANALOG(ImGuiKey_GamepadRStickDown, state.axis[3], +0.25, +1.0); + #undef MAP_BUTTON + #undef MAP_ANALOG + #undef IM_SATURATE +} + +bool ImGui_ImplEmscripten_Init(const char* target_id) +{ + ImGuiIO& io = ImGui::GetIO(); + IMGUI_CHECKVERSION(); + IM_ASSERT(io.BackendPlatformUserData == nullptr && "Already initialized a platform backend!"); + + ImGui_ImplEmscripten_BackendData* bd = IM_NEW(ImGui_ImplEmscripten_BackendData)(); + io.BackendPlatformUserData = (void*)bd; + io.BackendPlatformName = "imgui_impl_emscripten"; + io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors; // We can honor GetMouseCursor() values (optional) + + double width, height; + emscripten_get_element_css_size(target_id, &width, &height); + bd->Width = width; + bd->Height = height; + bd->Time = emscripten_performance_now(); + bd->LastMouseCursor = ImGuiMouseCursor_COUNT; + bd->ClipboardTextData = nullptr; + bd->GamepadsCount = 0; + + size_t target_id_size = strlen(target_id); + bd->TargetId = (char*)IM_ALLOC(target_id_size + 1); + memcpy(bd->TargetId, target_id, target_id_size + 1); + + io.DisplaySize = { (float)bd->Width, (float)bd->Height }; + + emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, &ImGui_ImplEmscripten_KeyCallback); + emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, &ImGui_ImplEmscripten_KeyCallback); + +#ifdef IMGUI_IMPL_EMSCRIPTEN_ENABLE_CLIPBOARD + ImGui_ImplEmscripten_SetupClipboardCallbacks(bd); +#endif + +#ifndef IMGUI_IMPL_EMSCRIPTEN_POINTER_EVENTS + emscripten_set_click_callback(target_id, NULL, false, &ImGui_ImplEmscripten_MouseCallback); + emscripten_set_mousedown_callback(target_id, NULL, false, &ImGui_ImplEmscripten_MouseCallback); + emscripten_set_mouseup_callback(target_id, NULL, false, &ImGui_ImplEmscripten_MouseCallback); + emscripten_set_dblclick_callback(target_id, NULL, false, &ImGui_ImplEmscripten_MouseCallback); + emscripten_set_mousemove_callback(target_id, NULL, false, &ImGui_ImplEmscripten_MouseCallback); + emscripten_set_mouseenter_callback(target_id, NULL, false, &ImGui_ImplEmscripten_MouseCallback); + emscripten_set_mouseleave_callback(target_id, NULL, false, &ImGui_ImplEmscripten_MouseCallback); +#else + ImGui_ImplEmscripten_SetupPointerEvents(bd); +#endif + + emscripten_set_wheel_callback(target_id, NULL, false, &ImGui_ImplEmscripten_WheelCallback); + + emscripten_set_focusin_callback(target_id, NULL, false, &ImGui_ImplEmscripten_FocusCallback); + emscripten_set_focusout_callback(target_id, NULL, false, &ImGui_ImplEmscripten_FocusCallback); + + emscripten_set_gamepadconnected_callback(NULL, false, &ImGui_ImplEmscripten_GamepadCallback); + emscripten_set_gamepaddisconnected_callback(NULL, false, &ImGui_ImplEmscripten_GamepadCallback); + + return true; +} + +void ImGui_ImplEmscripten_Shutdown() +{ + ImGui_ImplEmscripten_BackendData* bd = ImGui_ImplEmscripten_GetBackendData(); + + emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL); + emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL); + +#ifndef IMGUI_IMPL_EMSCRIPTEN_POINTER_EVENTS + emscripten_set_click_callback(bd->TargetId, NULL, false, NULL); + emscripten_set_mousedown_callback(bd->TargetId, NULL, false, NULL); + emscripten_set_mouseup_callback(bd->TargetId, NULL, false, NULL); + emscripten_set_dblclick_callback(bd->TargetId, NULL, false, NULL); + emscripten_set_mousemove_callback(bd->TargetId, NULL, false, NULL); + emscripten_set_mouseenter_callback(bd->TargetId, NULL, false, NULL); + emscripten_set_mouseleave_callback(bd->TargetId, NULL, false, NULL); +#else + ImGui_ImplEmscripten_ClearPointerEvents(bd); +#endif + + emscripten_set_wheel_callback(bd->TargetId, NULL, false, NULL); + + emscripten_set_focusin_callback(bd->TargetId, NULL, false, NULL); + emscripten_set_focusout_callback(bd->TargetId, NULL, false, NULL); + + emscripten_set_gamepadconnected_callback(NULL, false, NULL); + emscripten_set_gamepaddisconnected_callback(NULL, false, NULL); + + free((void*)bd->TargetId); + IM_DELETE(bd); +} + +void ImGui_ImplEmscripten_NewFrame() +{ + ImGuiIO& io = ImGui::GetIO(); + ImGui_ImplEmscripten_BackendData* bd = ImGui_ImplEmscripten_GetBackendData(io); + + double current_time = emscripten_performance_now(); + io.DeltaTime = (float)((current_time - bd->Time) / 1000.0); + bd->Time = current_time; + + // Update OS mouse cursor with the cursor requested by imgui + ImGuiMouseCursor mouse_cursor = io.MouseDrawCursor ? ImGuiMouseCursor_None : ImGui::GetMouseCursor(); + if (bd->LastMouseCursor != mouse_cursor) + { + bd->LastMouseCursor = mouse_cursor; + ImGui_ImplEmscripten_UpdateMouseCursor(io, mouse_cursor); + } + + ImGui_ImplEmscripten_UpdateGamepads(io); +} + +void ImGui_ImplEmscripten_UpdateCanvasSize(int width, int height) +{ + ImGuiIO& io = ImGui::GetIO(); + ImGui_ImplEmscripten_BackendData* bd = ImGui_ImplEmscripten_GetBackendData(io); + + bd->Width = width; + bd->Height = height; + + io.DisplaySize = { (float)bd->Width, (float)bd->Height }; +} + +#endif // #ifndef IMGUI_DISABLE diff --git a/backends/imgui_impl_emscripten.h b/backends/imgui_impl_emscripten.h new file mode 100644 index 000000000..d8b4312df --- /dev/null +++ b/backends/imgui_impl_emscripten.h @@ -0,0 +1,30 @@ +// dear imgui: Platform Backend for Emscripten +// This needs to be used along with a Renderer (e.g. OpenGL3) + +// Implemented features: +// [X] Platform: Clipboard support (with IMGUI_IMPL_EMSCRIPTEN_ENABLE_CLIPBOARD). +// [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen (with IMGUI_IMPL_EMSCRIPTEN_POINTER_EVENTS). +// [X] Platform: Keyboard support. +// [X] Platform: Gamepad support. +// [X] Platform: Mouse cursor shape and visibility (ImGuiBackendFlags_HasMouseCursors). Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. + +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp + +#pragma once +#include "imgui.h" // IMGUI_IMPL_API +#ifndef IMGUI_DISABLE + +// Follow "Getting Started" link and check examples/ folder to learn about using backends! +IMGUI_IMPL_API bool ImGui_ImplEmscripten_Init(const char* target_id); +IMGUI_IMPL_API void ImGui_ImplEmscripten_Shutdown(); +IMGUI_IMPL_API void ImGui_ImplEmscripten_NewFrame(); + +IMGUI_IMPL_API void ImGui_ImplEmscripten_UpdateCanvasSize(int width, int height); + +#endif // #ifndef IMGUI_DISABLE diff --git a/examples/example_emscripten_opengl3/.gitignore b/examples/example_emscripten_opengl3/.gitignore new file mode 100644 index 000000000..73cf2e81a --- /dev/null +++ b/examples/example_emscripten_opengl3/.gitignore @@ -0,0 +1 @@ +web/ diff --git a/examples/example_emscripten_opengl3/Makefile b/examples/example_emscripten_opengl3/Makefile new file mode 100644 index 000000000..03ff8610f --- /dev/null +++ b/examples/example_emscripten_opengl3/Makefile @@ -0,0 +1,102 @@ +# +# Makefile to use with Emscripten+opengl3 +# See https://emscripten.org/docs/getting_started/downloads.html +# for installation instructions. +# +# This Makefile assumes you have loaded emscripten's environment. +# (On Windows, you may need to execute emsdk_env.bat or encmdprompt.bat ahead) +# +# Running `make -f Makefile.emscripten` will produce three files: +# - web/index.html +# - web/index.js +# - web/index.wasm +# +# All three are needed to run the demo. + +CC = emcc +CXX = em++ +WEB_DIR = web +EXE = $(WEB_DIR)/index.html +IMGUI_DIR = ../.. +SOURCES = main.cpp +SOURCES += $(IMGUI_DIR)/imgui.cpp $(IMGUI_DIR)/imgui_demo.cpp $(IMGUI_DIR)/imgui_draw.cpp $(IMGUI_DIR)/imgui_tables.cpp $(IMGUI_DIR)/imgui_widgets.cpp +SOURCES += $(IMGUI_DIR)/backends/imgui_impl_emscripten.cpp $(IMGUI_DIR)/backends/imgui_impl_opengl3.cpp +OBJS = $(addsuffix .o, $(basename $(notdir $(SOURCES)))) +ifneq ($(OS),Windows_NT) +UNAME_S := $(shell uname -s) +endif +CPPFLAGS = -DIMGUI_IMPL_OPENGL_ES2 +LDFLAGS = +EMS = + +# Use pointer events for this example (they properly support Mouse/TouchScreen/Pen) +CPPFLAGS += -DIMGUI_IMPL_EMSCRIPTEN_POINTER_EVENTS + +# Enable clipboard support +CPPFLAGS += -DIMGUI_IMPL_EMSCRIPTEN_ENABLE_CLIPBOARD + +##--------------------------------------------------------------------- +## EMSCRIPTEN OPTIONS +##--------------------------------------------------------------------- + +# ("EMS" options gets added to both CPPFLAGS and LDFLAGS, whereas some options are for linker only) +EMS += -s DISABLE_EXCEPTION_CATCHING=1 +LDFLAGS += -s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -s NO_EXIT_RUNTIME=0 -s ASSERTIONS=1 + +# Build as single file (binary text encoded in .html file) +#LDFLAGS += -sSINGLE_FILE + +# Uncomment next line to fix possible rendering bugs with Emscripten version older then 1.39.0 (https://github.com/ocornut/imgui/issues/2877) +#EMS += -s BINARYEN_TRAP_MODE=clamp +#EMS += -s SAFE_HEAP=1 ## Adds overhead + +# Emscripten allows preloading a file or folder to be accessible at runtime. +# The Makefile for this example project suggests embedding the misc/fonts/ folder into our application, it will then be accessible as "/fonts" +# See documentation for more details: https://emscripten.org/docs/porting/files/packaging_files.html +# (Default value is 0. Set to 1 to enable file-system and include the misc/fonts/ folder as part of the build.) +USE_FILE_SYSTEM ?= 0 +ifeq ($(USE_FILE_SYSTEM), 0) +LDFLAGS += -s NO_FILESYSTEM=1 +CPPFLAGS += -DIMGUI_DISABLE_FILE_FUNCTIONS +endif +ifeq ($(USE_FILE_SYSTEM), 1) +LDFLAGS += --no-heap-copy --preload-file ../../misc/fonts@/fonts +endif + +##--------------------------------------------------------------------- +## FINAL BUILD FLAGS +##--------------------------------------------------------------------- + +CPPFLAGS += -I$(IMGUI_DIR) -I$(IMGUI_DIR)/backends +#CPPFLAGS += -g +CPPFLAGS += -Wall -Wformat -Os $(EMS) +LDFLAGS += --shell-file ../libs/emscripten/shell_minimal.html +LDFLAGS += $(EMS) + +##--------------------------------------------------------------------- +## BUILD RULES +##--------------------------------------------------------------------- + +%.o:%.cpp + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $< + +%.o:$(IMGUI_DIR)/%.cpp + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $< + +%.o:$(IMGUI_DIR)/backends/%.cpp + $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $< + +all: $(EXE) + @echo Build complete for $(EXE) + +$(WEB_DIR): + mkdir $@ + +serve: all + python3 -m http.server -d $(WEB_DIR) + +$(EXE): $(OBJS) $(WEB_DIR) + $(CXX) -o $@ $(OBJS) $(LDFLAGS) + +clean: + rm -rf $(OBJS) $(WEB_DIR) diff --git a/examples/example_emscripten_opengl3/main.cpp b/examples/example_emscripten_opengl3/main.cpp new file mode 100644 index 000000000..6a74704bb --- /dev/null +++ b/examples/example_emscripten_opengl3/main.cpp @@ -0,0 +1,204 @@ +// Dear ImGui: standalone example application for Emscripten + OpenGL 3, using programmable pipeline +// (Emscripten is a complete Open Source compiler toolchain to WebAssembly.) + +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp + +#include "imgui.h" +#include "imgui_impl_emscripten.h" +#include "imgui_impl_opengl3.h" + +#include +#include // Use GL ES 2 + +#include "../libs/emscripten/emscripten_mainloop_stub.h" + +#include +#include + +#define TARGET_ELEMENT_ID "#canvas" + +static EMSCRIPTEN_WEBGL_CONTEXT_HANDLE g_ContextHandle; +static int g_CanvasWidth; +static int g_CanvasHeight; + +static void ResizeCanvas(int width, int height) +{ + g_CanvasWidth = width; + g_CanvasHeight = height; + emscripten_set_canvas_element_size(TARGET_ELEMENT_ID, g_CanvasWidth, g_CanvasHeight); +} + +static bool CreateWebGLContext() +{ + EmscriptenWebGLContextAttributes attr{}; + emscripten_webgl_init_context_attributes(&attr); + attr.majorVersion = 1; + attr.minorVersion = 0; + attr.antialias = false; + attr.depth = false; + attr.stencil = false; + attr.alpha = true; + + g_ContextHandle = emscripten_webgl_create_context(TARGET_ELEMENT_ID, &attr); + if (g_ContextHandle != 0) + { + double width, height; + emscripten_get_element_css_size(TARGET_ELEMENT_ID, &width, &height); + ResizeCanvas((int)width, (int)height); + } + + return g_ContextHandle != 0; +} + +static bool WindowResizeCallback(int type, const EmscriptenUiEvent* event, void* user_data) +{ + ResizeCanvas(event->windowInnerWidth, event->windowInnerHeight); + ImGui_ImplEmscripten_UpdateCanvasSize(g_CanvasWidth, g_CanvasHeight); + return false; +} + +// Main code +int main(int, char**) +{ + const float main_scale = 1.f; + + // Decide GL+GLSL versions +#if defined(IMGUI_IMPL_OPENGL_ES2) + // GL ES 2.0 + GLSL 100 (WebGL 1.0) + const char* glsl_version = "#version 100"; +#elif defined(IMGUI_IMPL_OPENGL_ES3) + // GL ES 3.0 + GLSL 300 es (WebGL 2.0) + const char* glsl_version = "#version 300 es"; +#else + // GL 3.0 + GLSL 130 + const char* glsl_version = "#version 130"; +#endif + + if (!CreateWebGLContext()) + { + emscripten_console_error("Failed to create webgl context"); + return 1; + } + + emscripten_webgl_make_context_current(g_ContextHandle); + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, (em_ui_callback_func)&WindowResizeCallback); + + // Setup Dear ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); (void)io; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls + + // Setup Dear ImGui style + ImGui::StyleColorsDark(); + //ImGui::StyleColorsLight(); + + // Setup scaling + ImGuiStyle& style = ImGui::GetStyle(); + style.ScaleAllSizes(main_scale); // Bake a fixed style scale. (until we have a solution for dynamic style scaling, changing this requires resetting Style + calling this again) + style.FontScaleDpi = main_scale; // Set initial font scale. (using io.ConfigDpiScaleFonts=true makes this unnecessary. We leave both here for documentation purpose) + + // Setup Platform/Renderer backends + ImGui_ImplEmscripten_Init(TARGET_ELEMENT_ID); + ImGui_ImplOpenGL3_Init(glsl_version); + + // Load Fonts + // - If no fonts are loaded, dear imgui will use the default font. You can also load multiple fonts and use ImGui::PushFont()/PopFont() to select them. + // - AddFontFromFileTTF() will return the ImFont* so you can store it if you need to select the font among multiple. + // - If the file cannot be loaded, the function will return a nullptr. Please handle those errors in your application (e.g. use an assertion, or display an error and quit). + // - Use '#define IMGUI_ENABLE_FREETYPE' in your imconfig file to use Freetype for higher quality font rendering. + // - Read 'docs/FONTS.md' for more instructions and details. If you like the default font but want it to scale better, consider using the 'ProggyVector' from the same author! + // - Remember that in C/C++ if you want to include a backslash \ in a string literal you need to write a double backslash \\ ! + // - Our Emscripten build process allows embedding fonts to be accessible at runtime from the "fonts/" folder. See Makefile.emscripten for details. + //style.FontSizeBase = 20.0f; + //io.Fonts->AddFontDefault(); + //io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeui.ttf"); + //io.Fonts->AddFontFromFileTTF("../../misc/fonts/DroidSans.ttf"); + //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Roboto-Medium.ttf"); + //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Cousine-Regular.ttf"); + //ImFont* font = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\ArialUni.ttf"); + //IM_ASSERT(font != nullptr); + + // Our state + bool show_demo_window = true; + bool show_another_window = false; + ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f); + + // Main loop + // For an Emscripten build we are disabling file-system access, so let's not attempt to do a fopen() of the imgui.ini file. + // You may manually call LoadIniSettingsFromMemory() to load settings from your own storage. + io.IniFilename = nullptr; + EMSCRIPTEN_MAINLOOP_BEGIN + { + // Poll and handle events (inputs, window resize, etc.) + // You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs. + // - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data. + // - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data. + // Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags. + // Emscripten backend uses callbacks to handle events so there is no need in this! + + // Start the Dear ImGui frame + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplEmscripten_NewFrame(); + ImGui::NewFrame(); + + // 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!). + if (show_demo_window) + ImGui::ShowDemoWindow(&show_demo_window); + + // 2. Show a simple window that we create ourselves. We use a Begin/End pair to create a named window. + { + static float f = 0.0f; + static int counter = 0; + + ImGui::Begin("Hello, world!"); // Create a window called "Hello, world!" and append into it. + + ImGui::Text("This is some useful text."); // Display some text (you can use a format strings too) + ImGui::Checkbox("Demo Window", &show_demo_window); // Edit bools storing our window open/close state + ImGui::Checkbox("Another Window", &show_another_window); + + ImGui::SliderFloat("float", &f, 0.0f, 1.0f); // Edit 1 float using a slider from 0.0f to 1.0f + ImGui::ColorEdit3("clear color", (float*)&clear_color); // Edit 3 floats representing a color + + if (ImGui::Button("Button")) // Buttons return true when clicked (most widgets return true when edited/activated) + counter++; + ImGui::SameLine(); + ImGui::Text("counter = %d", counter); + + ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); + ImGui::End(); + } + + // 3. Show another simple window. + if (show_another_window) + { + ImGui::Begin("Another Window", &show_another_window); // Pass a pointer to our bool variable (the window will have a closing button that will clear the bool when clicked) + ImGui::Text("Hello from another window!"); + if (ImGui::Button("Close Me")) + show_another_window = false; + ImGui::End(); + } + + // Rendering + ImGui::Render(); + int display_w, display_h; + emscripten_webgl_get_drawing_buffer_size(g_ContextHandle, &display_w, &display_h); + glViewport(0, 0, display_w, display_h); + glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w); + glClear(GL_COLOR_BUFFER_BIT); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + } + EMSCRIPTEN_MAINLOOP_END; + + // Cleanup + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplEmscripten_NewFrame(); + ImGui::DestroyContext(); + + return 0; +} diff --git a/examples/libs/emscripten/shell_minimal.html b/examples/libs/emscripten/shell_minimal.html index bcf626261..5aea0813e 100644 --- a/examples/libs/emscripten/shell_minimal.html +++ b/examples/libs/emscripten/shell_minimal.html @@ -17,6 +17,7 @@ height: 100%; overflow: hidden; display: block; + touch-action: none; image-rendering: optimizeSpeed; image-rendering: -moz-crisp-edges; image-rendering: -o-crisp-edges;