diff DPF-Prymula-audioplugins/dpf/dgl/src/pugl-extra/wasm.c @ 3:84e66ea83026

DPF-Prymula-audioplugins-0.231015-2
author prymula <prymula76@outlook.com>
date Mon, 16 Oct 2023 21:53:34 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/DPF-Prymula-audioplugins/dpf/dgl/src/pugl-extra/wasm.c	Mon Oct 16 21:53:34 2023 +0200
@@ -0,0 +1,1141 @@
+// Copyright 2012-2022 David Robillard <d@drobilla.net>
+// Copyright 2021-2022 Filipe Coelho <falktx@falktx.com>
+// SPDX-License-Identifier: ISC
+
+#include "wasm.h"
+
+#include "../pugl-upstream/src/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
+
+// #define PUGL_WASM_AUTO_POINTER_LOCK
+// #define PUGL_WASM_NO_KEYBOARD_INPUT
+// #define PUGL_WASM_NO_MOUSEWHEEL_INPUT
+
+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 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);
+}
+
+#ifndef PUGL_WASM_NO_KEYBOARD_INPUT
+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 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;
+}
+#endif
+
+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__
+  if (!view->impl->isFullscreen) {
+    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;
+
+#ifdef __MOD_DEVICES__
+  const long canvasX = mouseEvent->canvasX;
+  const long canvasY = mouseEvent->canvasY;
+#else
+  const char* const className = view->world->className;
+  const double canvasX = mouseEvent->clientX - EM_ASM_DOUBLE({
+    var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
+    return canvasWrapper.getBoundingClientRect().x;
+  }, className);
+  const double canvasY = mouseEvent->clientY - EM_ASM_DOUBLE({
+    var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
+    return canvasWrapper.getBoundingClientRect().y;
+  }, className);
+#endif
+
+  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     = canvasX * scaleFactor;
+    event.button.y     = canvasY * 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 = canvasX * scaleFactor;
+      view->impl->lastMotion.y = event.motion.y = canvasY * 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     = canvasX * scaleFactor;
+    event.crossing.y     = canvasY * 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;
+  const char* const className = view->world->className;
+
+  if (impl->supportsTouch == PUGL_DONT_CARE) {
+    impl->supportsTouch = PUGL_TRUE;
+
+    // stop using mouse press events which conflict with touch
+    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__
+  if (!view->impl->isFullscreen) {
+    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);
+  }
+
+#ifdef __MOD_DEVICES__
+  const long canvasX = point->canvasX;
+  const long canvasY = point->canvasY;
+#else
+  const double canvasX = point->clientX - EM_ASM_DOUBLE({
+    var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
+    return canvasWrapper.getBoundingClientRect().x;
+  }, className);
+  const double canvasY = point->clientY - EM_ASM_DOUBLE({
+    var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
+    return canvasWrapper.getBoundingClientRect().y;
+  }, className);
+#endif
+
+  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      = canvasX * scaleFactor;
+    event.button.y      = canvasY * 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      = canvasX * scaleFactor;
+    event.button.y      = canvasY * 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     = canvasX * scaleFactor;
+    event.motion.y     = canvasY * 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;
+}
+
+#ifndef PUGL_WASM_NO_MOUSEWHEEL_INPUT
+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__
+  if (!view->impl->isFullscreen) {
+    scaleFactor /= EM_ASM_DOUBLE({
+      return parseFloat(
+        RegExp('^scale\\\((.*)\\\)$')
+        .exec(document.getElementById("pedalboard-dashboard").style.transform)[1]
+      );
+    }) * MOD_SCALE_FACTOR_MULT;
+  }
+#endif
+
+#ifdef __MOD_DEVICES__
+  const long canvasX = wheelEvent->mouse.canvasX;
+  const long canvasY = wheelEvent->mouse.canvasY;
+#else
+  const char* const className = view->world->className;
+  const double canvasX = wheelEvent->mouse.canvasX - EM_ASM_INT({
+    var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
+    return canvasWrapper.getBoundingClientRect().x;
+  }, className);
+  const double canvasY = wheelEvent->mouse.canvasY - EM_ASM_INT({
+    var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement;
+    return canvasWrapper.getBoundingClientRect().y;
+  }, className);
+#endif
+
+  PuglEvent event = {{PUGL_SCROLL, 0}};
+  event.scroll.time  = wheelEvent->mouse.timestamp / 1e3;
+  event.scroll.x     = canvasX;
+  event.scroll.y     = canvasY;
+  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;
+}
+#endif
+
+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;
+
+  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);
+
+  emscripten_set_canvas_element_size(view->world->className, width * scaleFactor, height * scaleFactor);
+  return EM_TRUE;
+}
+
+static EM_BOOL
+puglFullscreenChangeCallback(const int eventType, const EmscriptenFullscreenChangeEvent* const fscEvent, void* const userData)
+{
+  PuglView* const view = (PuglView*)userData;
+
+  view->impl->isFullscreen = fscEvent->isFullscreen;
+
+  double scaleFactor = emscripten_get_device_pixel_ratio();
+#ifdef __MOD_DEVICES__
+  scaleFactor *= MOD_SCALE_FACTOR_MULT;
+#endif
+  view->world->impl->scaleFactor = scaleFactor;
+
+  if (fscEvent->isFullscreen) {
+    PuglEvent event        = {{PUGL_CONFIGURE, 0}};
+    event.configure.x      = 0;
+    event.configure.y      = 0;
+    event.configure.width  = fscEvent->elementWidth * scaleFactor;
+    event.configure.height = fscEvent->elementHeight * scaleFactor;
+    puglDispatchEvent(view, &event);
+
+    emscripten_set_canvas_element_size(view->world->className,
+                                       fscEvent->elementWidth * scaleFactor,
+                                       fscEvent->elementHeight * scaleFactor);
+
+#ifdef __MOD_DEVICES__
+    EM_ASM({
+      document.getElementById("pedalboard-dashboard").style.transform = "scale(1.0)";
+    });
+#endif
+    return EM_TRUE;
+  }
+
+  return puglUiCallback(0, NULL, userData);
+}
+
+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);
+#ifndef PUGL_WASM_NO_KEYBOARD_INPUT
+//   emscripten_set_keypress_callback(className, view, false, puglKeyCallback);
+  emscripten_set_keydown_callback(className, view, false, puglKeyCallback);
+  emscripten_set_keyup_callback(className, view, false, puglKeyCallback);
+#endif
+  emscripten_set_touchstart_callback(className, view, false, puglTouchCallback);
+  emscripten_set_touchend_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglTouchCallback);
+  emscripten_set_touchmove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglTouchCallback);
+  emscripten_set_touchcancel_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglTouchCallback);
+  emscripten_set_mousedown_callback(className, view, false, puglMouseCallback);
+  emscripten_set_mouseup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglMouseCallback);
+  emscripten_set_mousemove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 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);
+#ifndef PUGL_WASM_NO_MOUSEWHEEL_INPUT
+  emscripten_set_wheel_callback(className, view, false, puglWheelCallback);
+#endif
+  emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglPointerLockChangeCallback);
+  emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglUiCallback);
+  emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglFullscreenChangeCallback);
+  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) {
+      // unregister the window events, to make sure no callbacks to old views are triggered
+      emscripten_set_touchend_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL);
+      emscripten_set_touchmove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL);
+      emscripten_set_touchcancel_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL);
+      emscripten_set_mouseup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL);
+      emscripten_set_mousemove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL);
+      emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL);
+      emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL);
+      emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL);
+      emscripten_set_visibilitychange_callback(NULL, false, NULL);
+      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;
+}