DPF

DISTRHO Plugin Framework
Log | Files | Refs | Submodules | README | LICENSE

commit 24fe04fcbdb2b07d550ca6784e0d03bfaf1957d3
parent 5c8e51c19a2b09f179715f45bc81b3d11ec918b0
Author: falkTX <falktx@falktx.com>
Date:   Wed, 28 Dec 2022 12:31:22 +0000

Include pugl wasm code directly, instead of in submodule

Signed-off-by: falkTX <falktx@falktx.com>

Diffstat:
Adgl/src/pugl-extra/wasm.c | 1032+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adgl/src/pugl-extra/wasm.h | 45+++++++++++++++++++++++++++++++++++++++++++++
Adgl/src/pugl-extra/wasm_gl.c | 228+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adgl/src/pugl-extra/wasm_stub.c | 26++++++++++++++++++++++++++
4 files changed, 1331 insertions(+), 0 deletions(-)

diff --git a/dgl/src/pugl-extra/wasm.c b/dgl/src/pugl-extra/wasm.c @@ -0,0 +1,1032 @@ +// Copyright 2012-2022 David Robillard <d@drobilla.net> +// Copyright 2021-2022 Filipe Coelho <falktx@falktx.com> +// SPDX-License-Identifier: ISC + +#include "wasm.h" + +#include "internal.h" + +#include <stdio.h> + +#include <emscripten/html5.h> + +#ifdef __cplusplus +# define PUGL_INIT_STRUCT \ + {} +#else +# define PUGL_INIT_STRUCT \ + { \ + 0 \ + } +#endif + +#ifdef __MOD_DEVICES__ +# define MOD_SCALE_FACTOR_MULT 1 +#endif + +PuglWorldInternals* +puglInitWorldInternals(const PuglWorldType type, const PuglWorldFlags flags) +{ + PuglWorldInternals* impl = + (PuglWorldInternals*)calloc(1, sizeof(PuglWorldInternals)); + + impl->scaleFactor = emscripten_get_device_pixel_ratio(); +#ifdef __MOD_DEVICES__ + impl->scaleFactor *= MOD_SCALE_FACTOR_MULT; +#endif + + printf("DONE: %s %d | -> %f\n", __func__, __LINE__, impl->scaleFactor); + + return impl; +} + +void* +puglGetNativeWorld(PuglWorld*) +{ + printf("DONE: %s %d\n", __func__, __LINE__); + return NULL; +} + +PuglInternals* +puglInitViewInternals(PuglWorld* const world) +{ + printf("DONE: %s %d\n", __func__, __LINE__); + PuglInternals* impl = (PuglInternals*)calloc(1, sizeof(PuglInternals)); + + impl->buttonPressTimeout = -1; + impl->supportsTouch = PUGL_DONT_CARE; // not yet known + +#ifdef PUGL_WASM_ASYNC_CLIPBOARD + impl->supportsClipboardRead = (PuglViewHintValue)EM_ASM_INT({ + if (typeof(navigator.clipboard) !== 'undefined' && typeof(navigator.clipboard.readText) === 'function' && window.isSecureContext) { + return 1; // PUGL_TRUE + } + return 0; // PUGL_FALSE + }); + + impl->supportsClipboardWrite = (PuglViewHintValue)EM_ASM_INT({ + if (typeof(navigator.clipboard) !== 'undefined' && typeof(navigator.clipboard.writeText) === 'function' && window.isSecureContext) { + return 1; // PUGL_TRUE + } + if (typeof(document.queryCommandSupported) !== 'undefined' && document.queryCommandSupported("copy")) { + return 1; // PUGL_TRUE + } + return 0; // PUGL_FALSE + }); +#endif + + return impl; +} + +static PuglStatus +puglDispatchEventWithContext(PuglView* const view, const PuglEvent* event) +{ + PuglStatus st0 = PUGL_SUCCESS; + PuglStatus st1 = PUGL_SUCCESS; + + if (!(st0 = view->backend->enter(view, NULL))) { + st0 = view->eventFunc(view, event); + st1 = view->backend->leave(view, NULL); + } + + return st0 ? st0 : st1; +} + +static PuglKey +keyCodeToSpecial(const unsigned long code, const unsigned long location) +{ + switch (code) { + case 0x08: return PUGL_KEY_BACKSPACE; + case 0x1B: return PUGL_KEY_ESCAPE; + case 0x2E: return PUGL_KEY_DELETE; + case 0x70: return PUGL_KEY_F1; + case 0x71: return PUGL_KEY_F2; + case 0x72: return PUGL_KEY_F3; + case 0x73: return PUGL_KEY_F4; + case 0x74: return PUGL_KEY_F5; + case 0x75: return PUGL_KEY_F6; + case 0x76: return PUGL_KEY_F7; + case 0x77: return PUGL_KEY_F8; + case 0x78: return PUGL_KEY_F9; + case 0x79: return PUGL_KEY_F10; + case 0x7A: return PUGL_KEY_F11; + case 0x7B: return PUGL_KEY_F12; + case 0x25: return PUGL_KEY_LEFT; + case 0x26: return PUGL_KEY_UP; + case 0x27: return PUGL_KEY_RIGHT; + case 0x28: return PUGL_KEY_DOWN; + case 0x21: return PUGL_KEY_PAGE_UP; + case 0x22: return PUGL_KEY_PAGE_DOWN; + case 0x24: return PUGL_KEY_HOME; + case 0x23: return PUGL_KEY_END; + case 0x2D: return PUGL_KEY_INSERT; + case 0x10: return location == DOM_KEY_LOCATION_RIGHT ? PUGL_KEY_SHIFT_R : PUGL_KEY_SHIFT_L; + case 0x11: return location == DOM_KEY_LOCATION_RIGHT ? PUGL_KEY_CTRL_R : PUGL_KEY_CTRL_L; + case 0x12: return location == DOM_KEY_LOCATION_RIGHT ? PUGL_KEY_ALT_R : PUGL_KEY_ALT_L; + case 0xE0: return location == DOM_KEY_LOCATION_RIGHT ? PUGL_KEY_SUPER_R : PUGL_KEY_SUPER_L; + case 0x5D: return PUGL_KEY_MENU; + case 0x14: return PUGL_KEY_CAPS_LOCK; + case 0x91: return PUGL_KEY_SCROLL_LOCK; + case 0x90: return PUGL_KEY_NUM_LOCK; + case 0x2C: return PUGL_KEY_PRINT_SCREEN; + case 0x13: return PUGL_KEY_PAUSE; + case '\r': return (PuglKey)'\r'; + default: break; + } + + return (PuglKey)0; +} + +static PuglMods +translateModifiers(const EM_BOOL ctrlKey, + const EM_BOOL shiftKey, + const EM_BOOL altKey, + const EM_BOOL metaKey) +{ + return (ctrlKey ? PUGL_MOD_CTRL : 0u) | + (shiftKey ? PUGL_MOD_SHIFT : 0u) | + (altKey ? PUGL_MOD_ALT : 0u) | + (metaKey ? PUGL_MOD_SUPER : 0u); +} + +static bool +decodeCharacterString(const unsigned long keyCode, + const EM_UTF8 key[EM_HTML5_SHORT_STRING_LEN_BYTES], + char str[8]) +{ + if (key[1] == 0) + { + str[0] = key[0]; + return true; + } + + return false; +} + +static EM_BOOL +puglKeyCallback(const int eventType, const EmscriptenKeyboardEvent* const keyEvent, void* const userData) +{ + PuglView* const view = (PuglView*)userData; + + if (!view->visible) { + return EM_FALSE; + } + + if (keyEvent->repeat && view->hints[PUGL_IGNORE_KEY_REPEAT]) + return EM_TRUE; + + PuglStatus st0 = PUGL_SUCCESS; + PuglStatus st1 = PUGL_SUCCESS; + + const uint state = translateModifiers(keyEvent->ctrlKey, + keyEvent->shiftKey, + keyEvent->altKey, + keyEvent->metaKey); + + const PuglKey special = keyCodeToSpecial(keyEvent->keyCode, keyEvent->location); + + uint key = keyEvent->key[0] >= ' ' && keyEvent->key[0] <= '~' && keyEvent->key[1] == '\0' + ? keyEvent->key[0] + : keyEvent->keyCode; + + if (key >= 'A' && key <= 'Z' && !keyEvent->shiftKey) + key += 'a' - 'A'; + + PuglEvent event = {{PUGL_NOTHING, 0}}; + event.key.type = eventType == EMSCRIPTEN_EVENT_KEYDOWN ? PUGL_KEY_PRESS : PUGL_KEY_RELEASE; + event.key.time = keyEvent->timestamp / 1e3; + // event.key.x = xevent.xkey.x; + // event.key.y = xevent.xkey.y; + // event.key.xRoot = xevent.xkey.x_root; + // event.key.yRoot = xevent.xkey.y_root; + event.key.key = special ? special : key; + event.key.keycode = keyEvent->keyCode; + event.key.state = state; + st0 = puglDispatchEventWithContext(view, &event); + + d_debug("key event \n" + "\tdown: %d\n" + "\trepeat: %d\n" + "\tlocation: %d\n" + "\tstate: 0x%x\n" + "\tkey[]: '%s'\n" + "\tcode[]: '%s'\n" + "\tlocale[]: '%s'\n" + "\tkeyCode: 0x%lx:'%c' [deprecated, use key]\n" + "\twhich: 0x%lx:'%c' [deprecated, use key, same as keycode?]\n" + "\tspecial: 0x%x", + eventType == EMSCRIPTEN_EVENT_KEYDOWN, + keyEvent->repeat, + keyEvent->location, + state, + keyEvent->key, + keyEvent->code, + keyEvent->locale, + keyEvent->keyCode, keyEvent->keyCode >= ' ' && keyEvent->keyCode <= '~' ? keyEvent->keyCode : 0, + keyEvent->which, keyEvent->which >= ' ' && keyEvent->which <= '~' ? keyEvent->which : 0, + special); + + if (event.type == PUGL_KEY_PRESS && !special && !(keyEvent->ctrlKey|keyEvent->altKey|keyEvent->metaKey)) { + char str[8] = PUGL_INIT_STRUCT; + + if (decodeCharacterString(keyEvent->keyCode, keyEvent->key, str)) { + d_debug("resulting string is '%s'", str); + + event.text.type = PUGL_TEXT; + event.text.character = event.key.key; + memcpy(event.text.string, str, sizeof(event.text.string)); + st1 = puglDispatchEventWithContext(view, &event); + } + } + + return (st0 ? st0 : st1) == PUGL_SUCCESS ? EM_TRUE : EM_FALSE; +} + +static EM_BOOL +puglMouseCallback(const int eventType, const EmscriptenMouseEvent* const mouseEvent, void* const userData) +{ + PuglView* const view = (PuglView*)userData; + + if (!view->visible) { + return EM_FALSE; + } + + PuglEvent event = {{PUGL_NOTHING, 0}}; + + const double time = mouseEvent->timestamp / 1e3; + const PuglMods state = translateModifiers(mouseEvent->ctrlKey, + mouseEvent->shiftKey, + mouseEvent->altKey, + mouseEvent->metaKey); + + double scaleFactor = view->world->impl->scaleFactor; +#ifdef __MOD_DEVICES__ + scaleFactor /= EM_ASM_DOUBLE({ + return parseFloat( + RegExp('^scale\\\((.*)\\\)$') + .exec(document.getElementById("pedalboard-dashboard").style.transform)[1] + ); + }) * MOD_SCALE_FACTOR_MULT; +#endif + + // workaround missing pointer lock callback, see https://github.com/emscripten-core/emscripten/issues/9681 + EmscriptenPointerlockChangeEvent e; + if (emscripten_get_pointerlock_status(&e) == EMSCRIPTEN_RESULT_SUCCESS) + view->impl->pointerLocked = e.isActive; + + switch (eventType) { + case EMSCRIPTEN_EVENT_MOUSEDOWN: + case EMSCRIPTEN_EVENT_MOUSEUP: + event.button.type = eventType == EMSCRIPTEN_EVENT_MOUSEDOWN ? PUGL_BUTTON_PRESS : PUGL_BUTTON_RELEASE; + event.button.time = time; + event.button.x = mouseEvent->targetX * scaleFactor; + event.button.y = mouseEvent->targetY * scaleFactor; + event.button.xRoot = mouseEvent->screenX * scaleFactor; + event.button.yRoot = mouseEvent->screenY * scaleFactor; + event.button.state = state; + switch (mouseEvent->button) { + case 1: + event.button.button = 2; + break; + case 2: + event.button.button = 1; + break; + default: + event.button.button = mouseEvent->button; + break; + } + break; + case EMSCRIPTEN_EVENT_MOUSEMOVE: + event.motion.type = PUGL_MOTION; + event.motion.time = time; + if (view->impl->pointerLocked) { + // adjust local values for delta + const double movementX = mouseEvent->movementX * scaleFactor; + const double movementY = mouseEvent->movementY * scaleFactor; + view->impl->lastMotion.x += movementX; + view->impl->lastMotion.y += movementY; + view->impl->lastMotion.xRoot += movementX; + view->impl->lastMotion.yRoot += movementY; + // now set x, y, xRoot and yRoot + event.motion.x = view->impl->lastMotion.x; + event.motion.y = view->impl->lastMotion.y; + event.motion.xRoot = view->impl->lastMotion.xRoot; + event.motion.yRoot = view->impl->lastMotion.yRoot; + } else { + // cache values for possible pointer lock movement later + view->impl->lastMotion.x = event.motion.x = mouseEvent->targetX * scaleFactor; + view->impl->lastMotion.y = event.motion.y = mouseEvent->targetY * scaleFactor; + view->impl->lastMotion.xRoot = event.motion.xRoot = mouseEvent->screenX * scaleFactor; + view->impl->lastMotion.yRoot = event.motion.yRoot = mouseEvent->screenY * scaleFactor; + } + event.motion.state = state; + break; + case EMSCRIPTEN_EVENT_MOUSEENTER: + case EMSCRIPTEN_EVENT_MOUSELEAVE: + event.crossing.type = eventType == EMSCRIPTEN_EVENT_MOUSEENTER ? PUGL_POINTER_IN : PUGL_POINTER_OUT; + event.crossing.time = time; + event.crossing.x = mouseEvent->targetX * scaleFactor; + event.crossing.y = mouseEvent->targetY * scaleFactor; + event.crossing.xRoot = mouseEvent->screenX * scaleFactor; + event.crossing.yRoot = mouseEvent->screenY * scaleFactor; + event.crossing.state = state; + event.crossing.mode = PUGL_CROSSING_NORMAL; + break; + } + + if (event.type == PUGL_NOTHING) + return EM_FALSE; + + puglDispatchEventWithContext(view, &event); + +#ifdef PUGL_WASM_AUTO_POINTER_LOCK + switch (eventType) { + case EMSCRIPTEN_EVENT_MOUSEDOWN: + emscripten_request_pointerlock(view->world->className, false); + break; + case EMSCRIPTEN_EVENT_MOUSEUP: + emscripten_exit_pointerlock(); + break; + } +#endif + + // note: we must always return false, otherwise canvas never gets keyboard input + return EM_FALSE; +} + +static void +puglTouchStartDelay(void* const userData) +{ + PuglView* const view = (PuglView*)userData; + PuglInternals* const impl = view->impl; + + impl->buttonPressTimeout = -1; + impl->nextButtonEvent.button.time += 2000; + puglDispatchEventWithContext(view, &impl->nextButtonEvent); +} + +static EM_BOOL +puglTouchCallback(const int eventType, const EmscriptenTouchEvent* const touchEvent, void* const userData) +{ + if (touchEvent->numTouches <= 0) { + return EM_FALSE; + } + + PuglView* const view = (PuglView*)userData; + PuglInternals* const impl = view->impl; + + if (impl->supportsTouch == PUGL_DONT_CARE) { + impl->supportsTouch = PUGL_TRUE; + + // stop using mouse press events which conflict with touch + const char* const className = view->world->className; + emscripten_set_mousedown_callback(className, view, false, NULL); + emscripten_set_mouseup_callback(className, view, false, NULL); + } + + if (!view->visible) { + return EM_FALSE; + } + + PuglEvent event = {{PUGL_NOTHING, 0}}; + + const double time = touchEvent->timestamp / 1e3; + const PuglMods state = translateModifiers(touchEvent->ctrlKey, + touchEvent->shiftKey, + touchEvent->altKey, + touchEvent->metaKey); + + double scaleFactor = view->world->impl->scaleFactor; +#ifdef __MOD_DEVICES__ + scaleFactor /= EM_ASM_DOUBLE({ + return parseFloat( + RegExp('^scale\\\((.*)\\\)$') + .exec(document.getElementById("pedalboard-dashboard").style.transform)[1] + ); + }) * MOD_SCALE_FACTOR_MULT; +#endif + + d_debug("touch %d|%s %d || %ld", + eventType, + eventType == EMSCRIPTEN_EVENT_TOUCHSTART ? "start" : + eventType == EMSCRIPTEN_EVENT_TOUCHEND ? "end" : "cancel", + touchEvent->numTouches, + impl->buttonPressTimeout); + + const EmscriptenTouchPoint* point = &touchEvent->touches[0]; + + if (impl->buttonPressTimeout != -1 || eventType == EMSCRIPTEN_EVENT_TOUCHCANCEL) { + // if we received an event while touch is active, trigger initial click now + if (impl->buttonPressTimeout != -1) { + emscripten_clear_timeout(impl->buttonPressTimeout); + impl->buttonPressTimeout = -1; + if (eventType != EMSCRIPTEN_EVENT_TOUCHCANCEL) { + impl->nextButtonEvent.button.button = 0; + } + } + impl->nextButtonEvent.button.time = time; + puglDispatchEventWithContext(view, &impl->nextButtonEvent); + } + + switch (eventType) { + case EMSCRIPTEN_EVENT_TOUCHEND: + case EMSCRIPTEN_EVENT_TOUCHCANCEL: + event.button.type = PUGL_BUTTON_RELEASE; + event.button.time = time; + event.button.button = eventType == EMSCRIPTEN_EVENT_TOUCHCANCEL ? 1 : 0; + event.button.x = point->targetX * scaleFactor; + event.button.y = point->targetY * scaleFactor; + event.button.xRoot = point->screenX * scaleFactor; + event.button.yRoot = point->screenY * scaleFactor; + event.button.state = state; + break; + + case EMSCRIPTEN_EVENT_TOUCHSTART: + // this event can be used for a couple of things, store it until we know more + event.button.type = PUGL_BUTTON_PRESS; + event.button.time = time; + event.button.button = 1; // if no other event occurs soon, treat it as right-click + event.button.x = point->targetX * scaleFactor; + event.button.y = point->targetY * scaleFactor; + event.button.xRoot = point->screenX * scaleFactor; + event.button.yRoot = point->screenY * scaleFactor; + event.button.state = state; + memcpy(&impl->nextButtonEvent, &event, sizeof(PuglEvent)); + impl->buttonPressTimeout = emscripten_set_timeout(puglTouchStartDelay, 2000, view); + // fall through, moving "mouse" to touch position + + case EMSCRIPTEN_EVENT_TOUCHMOVE: + event.motion.type = PUGL_MOTION; + event.motion.time = time; + event.motion.x = point->targetX * scaleFactor; + event.motion.y = point->targetY * scaleFactor; + event.motion.xRoot = point->screenX * scaleFactor; + event.motion.yRoot = point->screenY * scaleFactor; + event.motion.state = state; + break; + } + + if (event.type == PUGL_NOTHING) + return EM_FALSE; + + puglDispatchEventWithContext(view, &event); + + // FIXME we must always return false?? + return EM_FALSE; +} + +static EM_BOOL +puglFocusCallback(const int eventType, const EmscriptenFocusEvent* /*const focusEvent*/, void* const userData) +{ + PuglView* const view = (PuglView*)userData; + + if (!view->visible) { + return EM_FALSE; + } + + d_debug("focus %d|%s", eventType, eventType == EMSCRIPTEN_EVENT_FOCUSIN ? "focus-in" : "focus-out"); + + PuglEvent event = {{eventType == EMSCRIPTEN_EVENT_FOCUSIN ? PUGL_FOCUS_IN : PUGL_FOCUS_OUT, 0}}; + event.focus.mode = PUGL_CROSSING_NORMAL; + + puglDispatchEventWithContext(view, &event); + + // note: we must always return false, otherwise canvas never gets proper focus + return EM_FALSE; +} + +static EM_BOOL +puglPointerLockChangeCallback(const int eventType, const EmscriptenPointerlockChangeEvent* event, void* const userData) +{ + PuglView* const view = (PuglView*)userData; + + view->impl->pointerLocked = event->isActive; + return EM_TRUE; +} + +static EM_BOOL +puglWheelCallback(const int eventType, const EmscriptenWheelEvent* const wheelEvent, void* const userData) +{ + PuglView* const view = (PuglView*)userData; + + if (!view->visible) { + return EM_FALSE; + } + + double scaleFactor = view->world->impl->scaleFactor; +#ifdef __MOD_DEVICES__ + scaleFactor /= EM_ASM_DOUBLE({ + return parseFloat( + RegExp('^scale\\\((.*)\\\)$') + .exec(document.getElementById("pedalboard-dashboard").style.transform)[1] + ); + }) * MOD_SCALE_FACTOR_MULT; +#endif + + PuglEvent event = {{PUGL_SCROLL, 0}}; + event.scroll.time = wheelEvent->mouse.timestamp / 1e3; + event.scroll.x = wheelEvent->mouse.targetX; + event.scroll.y = wheelEvent->mouse.targetY; + event.scroll.xRoot = wheelEvent->mouse.screenX; + event.scroll.yRoot = wheelEvent->mouse.screenY; + event.scroll.state = translateModifiers(wheelEvent->mouse.ctrlKey, + wheelEvent->mouse.shiftKey, + wheelEvent->mouse.altKey, + wheelEvent->mouse.metaKey); + event.scroll.direction = PUGL_SCROLL_SMOOTH; + // FIXME handle wheelEvent->deltaMode + event.scroll.dx = wheelEvent->deltaX * 0.01 * scaleFactor; + event.scroll.dy = -wheelEvent->deltaY * 0.01 * scaleFactor; + + return puglDispatchEventWithContext(view, &event) == PUGL_SUCCESS ? EM_TRUE : EM_FALSE; +} + +static EM_BOOL +puglUiCallback(const int eventType, const EmscriptenUiEvent* const uiEvent, void* const userData) +{ + PuglView* const view = (PuglView*)userData; + const char* const className = view->world->className; + + // FIXME + const int width = EM_ASM_INT({ + var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement; + canvasWrapper.style.setProperty("--device-pixel-ratio", window.devicePixelRatio); + return canvasWrapper.clientWidth; + }, className); + + const int height = EM_ASM_INT({ + var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement; + return canvasWrapper.clientHeight; + }, className); + + if (!width || !height) + return EM_FALSE; + + double scaleFactor = emscripten_get_device_pixel_ratio(); +#ifdef __MOD_DEVICES__ + scaleFactor *= MOD_SCALE_FACTOR_MULT; +#endif + view->world->impl->scaleFactor = scaleFactor; + + emscripten_set_canvas_element_size(view->world->className, width * scaleFactor, height * scaleFactor); + + PuglEvent event = {{PUGL_CONFIGURE, 0}}; + event.configure.x = view->frame.x; + event.configure.y = view->frame.y; + event.configure.width = width * scaleFactor; + event.configure.height = height * scaleFactor; + puglDispatchEvent(view, &event); + return EM_TRUE; +} + +static EM_BOOL +puglVisibilityChangeCallback(const int eventType, const EmscriptenVisibilityChangeEvent* const visibilityChangeEvent, void* const userData) +{ + PuglView* const view = (PuglView*)userData; + + view->visible = visibilityChangeEvent->hidden == EM_FALSE; + PuglEvent event = {{ view->visible ? PUGL_MAP : PUGL_UNMAP, 0}}; + puglDispatchEvent(view, &event); + return EM_FALSE; +} + +PuglStatus +puglRealize(PuglView* const view) +{ + printf("TODO: %s %d\n", __func__, __LINE__); + PuglStatus st = PUGL_SUCCESS; + + // Ensure that we do not have a parent + if (view->parent) { + printf("TODO: %s %d\n", __func__, __LINE__); + return PUGL_FAILURE; + } + + if (!view->backend || !view->backend->configure) { + printf("TODO: %s %d\n", __func__, __LINE__); + return PUGL_BAD_BACKEND; + } + + const char* const className = view->world->className; + d_stdout("className is %s", className); + + // Set the size to the default if it has not already been set + if (view->frame.width <= 0.0 && view->frame.height <= 0.0) { + PuglViewSize defaultSize = view->sizeHints[PUGL_DEFAULT_SIZE]; + if (!defaultSize.width || !defaultSize.height) { + return PUGL_BAD_CONFIGURATION; + } + + view->frame.width = defaultSize.width; + view->frame.height = defaultSize.height; + } + + // Configure and create the backend + if ((st = view->backend->configure(view)) || (st = view->backend->create(view))) { + view->backend->destroy(view); + return st; + } + + if (view->title) { + puglSetWindowTitle(view, view->title); + } + + puglDispatchSimpleEvent(view, PUGL_CREATE); + + PuglEvent event = {{PUGL_CONFIGURE, 0}}; + event.configure.x = view->frame.x; + event.configure.y = view->frame.y; + event.configure.width = view->frame.width; + event.configure.height = view->frame.height; + puglDispatchEvent(view, &event); + + EM_ASM({ + var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement; + canvasWrapper.style.setProperty("--device-pixel-ratio", window.devicePixelRatio); + }, className); + + emscripten_set_canvas_element_size(className, view->frame.width, view->frame.height); +// emscripten_set_keypress_callback(className, view, false, puglKeyCallback); + emscripten_set_keydown_callback(className, view, false, puglKeyCallback); + emscripten_set_keyup_callback(className, view, false, puglKeyCallback); + emscripten_set_touchstart_callback(className, view, false, puglTouchCallback); + emscripten_set_touchend_callback(className, view, false, puglTouchCallback); + emscripten_set_touchmove_callback(className, view, false, puglTouchCallback); + emscripten_set_touchcancel_callback(className, view, false, puglTouchCallback); + emscripten_set_mousedown_callback(className, view, false, puglMouseCallback); + emscripten_set_mouseup_callback(className, view, false, puglMouseCallback); + emscripten_set_mousemove_callback(className, view, false, puglMouseCallback); + emscripten_set_mouseenter_callback(className, view, false, puglMouseCallback); + emscripten_set_mouseleave_callback(className, view, false, puglMouseCallback); + emscripten_set_focusin_callback(className, view, false, puglFocusCallback); + emscripten_set_focusout_callback(className, view, false, puglFocusCallback); + emscripten_set_wheel_callback(className, view, false, puglWheelCallback); + emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglPointerLockChangeCallback); + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglUiCallback); + emscripten_set_visibilitychange_callback(view, false, puglVisibilityChangeCallback); + + printf("TODO: %s %d\n", __func__, __LINE__); + return PUGL_SUCCESS; +} + +PuglStatus +puglShow(PuglView* const view) +{ + view->visible = true; + view->impl->needsRepaint = true; + return puglPostRedisplay(view); +} + +PuglStatus +puglHide(PuglView* const view) +{ + view->visible = false; + return PUGL_FAILURE; +} + +void +puglFreeViewInternals(PuglView* const view) +{ + printf("DONE: %s %d\n", __func__, __LINE__); + if (view && view->impl) { + if (view->backend) { + view->backend->destroy(view); + } + free(view->impl->clipboardData); + free(view->impl->timers); + free(view->impl); + } +} + +void +puglFreeWorldInternals(PuglWorld* const world) +{ + printf("DONE: %s %d\n", __func__, __LINE__); + free(world->impl); +} + +PuglStatus +puglGrabFocus(PuglView*) +{ + return PUGL_FAILURE; +} + +double +puglGetScaleFactor(const PuglView* const view) +{ + printf("DONE: %s %d\n", __func__, __LINE__); + return view->world->impl->scaleFactor; +} + +double +puglGetTime(const PuglWorld*) +{ + return emscripten_get_now() / 1e3; +} + +PuglStatus +puglUpdate(PuglWorld* const world, const double timeout) +{ + for (size_t i = 0; i < world->numViews; ++i) { + PuglView* const view = world->views[i]; + + if (!view->visible) { + continue; + } + + puglDispatchSimpleEvent(view, PUGL_UPDATE); + + if (!view->impl->needsRepaint) { + continue; + } + + view->impl->needsRepaint = false; + + PuglEvent event = {{PUGL_EXPOSE, 0}}; + event.expose.x = view->frame.x; + event.expose.y = view->frame.y; + event.expose.width = view->frame.width; + event.expose.height = view->frame.height; + puglDispatchEvent(view, &event); + } + + return PUGL_SUCCESS; +} + +PuglStatus +puglPostRedisplay(PuglView* const view) +{ + view->impl->needsRepaint = true; + return PUGL_SUCCESS; +} + +PuglStatus +puglPostRedisplayRect(PuglView* const view, const PuglRect rect) +{ + view->impl->needsRepaint = true; + return PUGL_FAILURE; +} + +PuglNativeView +puglGetNativeView(PuglView* const view) +{ + return 0; +} + +PuglStatus +puglSetWindowTitle(PuglView* const view, const char* const title) +{ + puglSetString(&view->title, title); + emscripten_set_window_title(title); + return PUGL_SUCCESS; +} + +PuglStatus +puglSetSizeHint(PuglView* const view, + const PuglSizeHint hint, + const PuglSpan width, + const PuglSpan height) +{ + view->sizeHints[hint].width = width; + view->sizeHints[hint].height = height; + return PUGL_SUCCESS; +} + +static EM_BOOL +puglTimerLoopCallback(double timeout, void* const arg) +{ + PuglTimer* const timer = (PuglTimer*)arg; + PuglInternals* const impl = timer->view->impl; + + // only handle active timers + for (uint32_t i=0; i<impl->numTimers; ++i) + { + if (impl->timers[i].id == timer->id) + { + PuglEvent event = {{PUGL_TIMER, 0}}; + event.timer.id = timer->id; + puglDispatchEventWithContext(timer->view, &event); + return EM_TRUE; + } + } + + return EM_FALSE; + + // unused + (void)timeout; +} + +PuglStatus +puglStartTimer(PuglView* const view, const uintptr_t id, const double timeout) +{ + printf("DONE: %s %d\n", __func__, __LINE__); + PuglInternals* const impl = view->impl; + const uint32_t timerIndex = impl->numTimers++; + + if (impl->timers == NULL) + impl->timers = (PuglTimer*)malloc(sizeof(PuglTimer)); + else + impl->timers = (PuglTimer*)realloc(impl->timers, sizeof(PuglTimer) * timerIndex); + + PuglTimer* const timer = &impl->timers[timerIndex]; + timer->view = view; + timer->id = id; + + emscripten_set_timeout_loop(puglTimerLoopCallback, timeout * 1e3, timer); + return PUGL_SUCCESS; +} + +PuglStatus +puglStopTimer(PuglView* const view, const uintptr_t id) +{ + printf("DONE: %s %d\n", __func__, __LINE__); + PuglInternals* const impl = view->impl; + + if (impl->timers == NULL || impl->numTimers == 0) + return PUGL_FAILURE; + + for (uint32_t i=0; i<impl->numTimers; ++i) + { + if (impl->timers[i].id == id) + { + memmove(impl->timers + i, impl->timers + (i + 1), sizeof(PuglTimer) * (impl->numTimers - 1)); + --impl->numTimers; + return PUGL_SUCCESS; + } + } + + return PUGL_FAILURE; +} + +#ifdef PUGL_WASM_ASYNC_CLIPBOARD +EM_JS(char*, puglGetAsyncClipboardData, (), { + var text = Asyncify.handleSleep(function(wakeUp) { + navigator.clipboard.readText() + .then(function(text) { + wakeUp(text); + }) + .catch(function() { + wakeUp(""); + }); + }); + if (!text.length) { + return null; + } + var length = lengthBytesUTF8(text) + 1; + var str = _malloc(length); + stringToUTF8(text, str, length); + return str; +}); +#endif + +PuglStatus +puglPaste(PuglView* const view) +{ +#ifdef PUGL_WASM_ASYNC_CLIPBOARD + // abort early if we already know it is not supported + if (view->impl->supportsClipboardRead == PUGL_FALSE) { + return PUGL_UNSUPPORTED; + } + + free(view->impl->clipboardData); + view->impl->clipboardData = puglGetAsyncClipboardData(); +#endif + + if (view->impl->clipboardData == NULL) { + return PUGL_FAILURE; + } + + const PuglDataOfferEvent offer = { + PUGL_DATA_OFFER, + 0, + emscripten_get_now() / 1e3, + }; + + PuglEvent offerEvent; + offerEvent.offer = offer; + puglDispatchEvent(view, &offerEvent); + return PUGL_SUCCESS; +} + +PuglStatus +puglAcceptOffer(PuglView* const view, + const PuglDataOfferEvent* const offer, + const uint32_t typeIndex) +{ + if (typeIndex != 0) { + return PUGL_UNSUPPORTED; + } + + const PuglDataEvent data = { + PUGL_DATA, + 0, + emscripten_get_now() / 1e3, + 0, + }; + + PuglEvent dataEvent; + dataEvent.data = data; + puglDispatchEvent(view, &dataEvent); + return PUGL_SUCCESS; +} + +uint32_t +puglGetNumClipboardTypes(const PuglView* const view) +{ + return view->impl->clipboardData != NULL ? 1u : 0u; +} + +const char* +puglGetClipboardType(const PuglView* const view, const uint32_t typeIndex) +{ + return (typeIndex == 0 && view->impl->clipboardData != NULL) + ? "text/plain" + : NULL; +} + +const void* +puglGetClipboard(PuglView* const view, + const uint32_t typeIndex, + size_t* const len) +{ + return view->impl->clipboardData; +} + +PuglStatus +puglSetClipboard(PuglView* const view, + const char* const type, + const void* const data, + const size_t len) +{ + // only utf8 text supported for now + if (type != NULL && strcmp(type, "text/plain") != 0) { + return PUGL_UNSUPPORTED; + } + + const char* const className = view->world->className; + const char* const text = (const char*)data; + +#ifdef PUGL_WASM_ASYNC_CLIPBOARD + // abort early if we already know it is not supported + if (view->impl->supportsClipboardWrite == PUGL_FALSE) { + return PUGL_UNSUPPORTED; + } +#else + puglSetString(&view->impl->clipboardData, text); +#endif + + EM_ASM({ + if (typeof(navigator.clipboard) !== 'undefined' && typeof(navigator.clipboard.writeText) === 'function' && window.isSecureContext) { + navigator.clipboard.writeText(UTF8ToString($1)); + } else { + var canvasClipboardObjName = UTF8ToString($0) + "_clipboard"; + var canvasClipboardElem = document.getElementById(canvasClipboardObjName); + + if (!canvasClipboardElem) { + canvasClipboardElem = document.createElement('textarea'); + canvasClipboardElem.id = canvasClipboardObjName; + canvasClipboardElem.style.position = 'fixed'; + canvasClipboardElem.style.whiteSpace = 'pre'; + canvasClipboardElem.style.zIndex = '-1'; + canvasClipboardElem.setAttribute('readonly', true); + document.body.appendChild(canvasClipboardElem); + } + + canvasClipboardElem.textContent = UTF8ToString($1); + canvasClipboardElem.select(); + document.execCommand("copy"); + } + }, className, text); + + // FIXME proper return status + return PUGL_SUCCESS; +} + +PuglStatus +puglSetCursor(PuglView* const view, const PuglCursor cursor) +{ + printf("TODO: %s %d\n", __func__, __LINE__); + return PUGL_FAILURE; +} + +PuglStatus +puglSetTransientParent(PuglView* const view, const PuglNativeView parent) +{ + printf("TODO: %s %d\n", __func__, __LINE__); + view->transientParent = parent; + return PUGL_FAILURE; +} + +PuglStatus +puglSetPosition(PuglView* const view, const int x, const int y) +{ + printf("TODO: %s %d\n", __func__, __LINE__); + + if (x > INT16_MAX || y > INT16_MAX) { + return PUGL_BAD_PARAMETER; + } + + view->frame.x = (PuglCoord)x; + view->frame.y = (PuglCoord)y; + return PUGL_FAILURE; +} diff --git a/dgl/src/pugl-extra/wasm.h b/dgl/src/pugl-extra/wasm.h @@ -0,0 +1,45 @@ +// Copyright 2012-2022 David Robillard <d@drobilla.net> +// Copyright 2021-2022 Filipe Coelho <falktx@falktx.com> +// SPDX-License-Identifier: ISC + +#ifndef PUGL_SRC_WASM_H +#define PUGL_SRC_WASM_H + +// #include "attributes.h" +#include "types.h" + +#include "pugl/pugl.h" + +// #define PUGL_WASM_ASYNC_CLIPBOARD + +struct PuglTimer { + PuglView* view; + uintptr_t id; +}; + +struct PuglWorldInternalsImpl { + double scaleFactor; +}; + +struct LastMotionValues { + double x, y, xRoot, yRoot; +}; + +struct PuglInternalsImpl { + PuglSurface* surface; + bool needsRepaint; + bool pointerLocked; + uint32_t numTimers; + LastMotionValues lastMotion; + long buttonPressTimeout; + PuglEvent nextButtonEvent; +#ifdef PUGL_WASM_ASYNC_CLIPBOARD + PuglViewHintValue supportsClipboardRead; + PuglViewHintValue supportsClipboardWrite; +#endif + PuglViewHintValue supportsTouch; + char* clipboardData; + struct PuglTimer* timers; +}; + +#endif // PUGL_SRC_WASM_H diff --git a/dgl/src/pugl-extra/wasm_gl.c b/dgl/src/pugl-extra/wasm_gl.c @@ -0,0 +1,228 @@ +// Copyright 2012-2022 David Robillard <d@drobilla.net> +// Copyright 2021-2022 Filipe Coelho <falktx@falktx.com> +// SPDX-License-Identifier: ISC + +#include "stub.h" +#include "wasm.h" + +#include "pugl/pugl.h" + +#include <stdio.h> +#include <stdlib.h> + +#include <EGL/egl.h> + +// for performance reasons we can keep a single EGL context always active +#define PUGL_WASM_SINGLE_EGL_CONTEXT + +typedef struct { + EGLDisplay display; + EGLConfig config; + EGLContext context; + EGLSurface surface; +} PuglWasmGlSurface; + +static EGLint +puglWasmGlHintValue(const int value) +{ + return value == PUGL_DONT_CARE ? EGL_DONT_CARE : value; +} + +static int +puglWasmGlGetAttrib(const EGLDisplay display, + const EGLConfig config, + const EGLint attrib) +{ + EGLint value = 0; + eglGetConfigAttrib(display, config, attrib, &value); + return value; +} + +static PuglStatus +puglWasmGlConfigure(PuglView* view) +{ + PuglInternals* const impl = view->impl; + + const EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + + if (display == EGL_NO_DISPLAY) { + return PUGL_CREATE_CONTEXT_FAILED; + } + + int major, minor; + if (eglInitialize(display, &major, &minor) != EGL_TRUE) { + return PUGL_CREATE_CONTEXT_FAILED; + } + + EGLConfig config; + int numConfigs; + + if (eglGetConfigs(display, &config, 1, &numConfigs) != EGL_TRUE || numConfigs != 1) { + eglTerminate(display); + return PUGL_CREATE_CONTEXT_FAILED; + } + + // clang-format off + const EGLint attrs[] = { + /* + GLX_X_RENDERABLE, True, + GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR, + GLX_DRAWABLE_TYPE, GLX_WINDOW_BIT, + GLX_RENDER_TYPE, GLX_RGBA_BIT, + EGL_SAMPLE_BUFFERS, view->hints[PUGL_MULTI_SAMPLE] ? 1 : 0, + */ + EGL_SAMPLES, puglWasmGlHintValue(view->hints[PUGL_SAMPLES]), + EGL_RED_SIZE, puglWasmGlHintValue(view->hints[PUGL_RED_BITS]), + EGL_GREEN_SIZE, puglWasmGlHintValue(view->hints[PUGL_GREEN_BITS]), + EGL_BLUE_SIZE, puglWasmGlHintValue(view->hints[PUGL_BLUE_BITS]), + EGL_ALPHA_SIZE, puglWasmGlHintValue(view->hints[PUGL_ALPHA_BITS]), + EGL_DEPTH_SIZE, puglWasmGlHintValue(view->hints[PUGL_DEPTH_BITS]), + EGL_STENCIL_SIZE, puglWasmGlHintValue(view->hints[PUGL_STENCIL_BITS]), + EGL_NONE + }; + // clang-format on + + if (eglChooseConfig(display, attrs, &config, 1, &numConfigs) != EGL_TRUE || numConfigs != 1) { + eglTerminate(display); + return PUGL_CREATE_CONTEXT_FAILED; + } + + PuglWasmGlSurface* const surface = + (PuglWasmGlSurface*)calloc(1, sizeof(PuglWasmGlSurface)); + impl->surface = surface; + + surface->display = display; + surface->config = config; + surface->context = EGL_NO_SURFACE; + surface->surface = EGL_NO_CONTEXT; + + view->hints[PUGL_RED_BITS] = + puglWasmGlGetAttrib(display, config, EGL_RED_SIZE); + view->hints[PUGL_GREEN_BITS] = + puglWasmGlGetAttrib(display, config, EGL_GREEN_SIZE); + view->hints[PUGL_BLUE_BITS] = + puglWasmGlGetAttrib(display, config, EGL_BLUE_SIZE); + view->hints[PUGL_ALPHA_BITS] = + puglWasmGlGetAttrib(display, config, EGL_ALPHA_SIZE); + view->hints[PUGL_DEPTH_BITS] = + puglWasmGlGetAttrib(display, config, EGL_DEPTH_SIZE); + view->hints[PUGL_STENCIL_BITS] = + puglWasmGlGetAttrib(display, config, EGL_STENCIL_SIZE); + view->hints[PUGL_SAMPLES] = + puglWasmGlGetAttrib(display, config, EGL_SAMPLES); + + // double-buffering is always enabled for EGL + view->hints[PUGL_DOUBLE_BUFFER] = 1; + + return PUGL_SUCCESS; +} + +PUGL_WARN_UNUSED_RESULT +static PuglStatus +puglWasmGlEnter(PuglView* view, const PuglExposeEvent* PUGL_UNUSED(expose)) +{ + PuglWasmGlSurface* const surface = (PuglWasmGlSurface*)view->impl->surface; + if (!surface || !surface->context || !surface->surface) { + return PUGL_FAILURE; + } + +#ifndef PUGL_WASM_SINGLE_EGL_CONTEXT + return eglMakeCurrent(surface->display, surface->surface, surface->surface, surface->context) ? PUGL_SUCCESS : PUGL_FAILURE; +#else + return PUGL_SUCCESS; +#endif +} + +PUGL_WARN_UNUSED_RESULT +static PuglStatus +puglWasmGlLeave(PuglView* view, const PuglExposeEvent* expose) +{ + PuglWasmGlSurface* const surface = (PuglWasmGlSurface*)view->impl->surface; + + if (expose) { // note: swap buffers always enabled for EGL + eglSwapBuffers(surface->display, surface->surface); + } + +#ifndef PUGL_WASM_SINGLE_EGL_CONTEXT + return eglMakeCurrent(surface->display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT) ? PUGL_SUCCESS : PUGL_FAILURE; +#else + return PUGL_SUCCESS; +#endif +} + +static PuglStatus +puglWasmGlCreate(PuglView* view) +{ + PuglWasmGlSurface* const surface = (PuglWasmGlSurface*)view->impl->surface; + const EGLDisplay display = surface->display; + const EGLConfig config = surface->config; + + const EGLint attrs[] = { + EGL_CONTEXT_CLIENT_VERSION, + view->hints[PUGL_CONTEXT_VERSION_MAJOR], + + EGL_CONTEXT_MAJOR_VERSION, + view->hints[PUGL_CONTEXT_VERSION_MAJOR], + + /* + EGL_CONTEXT_MINOR_VERSION, + view->hints[PUGL_CONTEXT_VERSION_MINOR], + + EGL_CONTEXT_OPENGL_DEBUG, + (view->hints[PUGL_USE_DEBUG_CONTEXT] ? EGL_TRUE : EGL_FALSE), + + EGL_CONTEXT_OPENGL_PROFILE_MASK, + (view->hints[PUGL_USE_COMPAT_PROFILE] + ? EGL_CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT + : EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT), + */ + + EGL_NONE + }; + + surface->context = eglCreateContext(display, config, EGL_NO_CONTEXT, attrs); + + if (surface->context == EGL_NO_CONTEXT) { + return PUGL_CREATE_CONTEXT_FAILED; + } + + surface->surface = eglCreateWindowSurface(display, config, 0, NULL); + + if (surface->surface == EGL_NO_SURFACE) { + return PUGL_CREATE_CONTEXT_FAILED; + } + +#ifdef PUGL_WASM_SINGLE_EGL_CONTEXT + eglMakeCurrent(surface->display, surface->surface, surface->surface, surface->context); +#endif + + return PUGL_SUCCESS; +} + +static void +puglWasmGlDestroy(PuglView* view) +{ + PuglWasmGlSurface* surface = (PuglWasmGlSurface*)view->impl->surface; + if (surface) { + const EGLDisplay display = surface->display; + if (surface->surface != EGL_NO_SURFACE) + eglDestroySurface(display, surface->surface); + if (surface->context != EGL_NO_CONTEXT) + eglDestroyContext(display, surface->context); + eglTerminate(display); + free(surface); + view->impl->surface = NULL; + } +} + +const PuglBackend* +puglGlBackend(void) +{ + static const PuglBackend backend = {puglWasmGlConfigure, + puglWasmGlCreate, + puglWasmGlDestroy, + puglWasmGlEnter, + puglWasmGlLeave, + puglStubGetContext}; + return &backend; +} diff --git a/dgl/src/pugl-extra/wasm_stub.c b/dgl/src/pugl-extra/wasm_stub.c @@ -0,0 +1,26 @@ +// Copyright 2012-2022 David Robillard <d@drobilla.net> +// Copyright 2021-2022 Filipe Coelho <falktx@falktx.com> +// SPDX-License-Identifier: ISC + +#include "pugl/stub.h" + +#include "stub.h" +// #include "types.h" +// #include "wasm.h" + +#include "pugl/pugl.h" + +const PuglBackend* +puglStubBackend(void) +{ + static const PuglBackend backend = { + puglStubConfigure, + puglStubCreate, + puglStubDestroy, + puglStubEnter, + puglStubLeave, + puglStubGetContext, + }; + + return &backend; +}