wasm.c (36473B)
1 // Copyright 2012-2022 David Robillard <d@drobilla.net> 2 // Copyright 2021-2022 Filipe Coelho <falktx@falktx.com> 3 // SPDX-License-Identifier: ISC 4 5 #include "wasm.h" 6 7 #include "../pugl-upstream/src/internal.h" 8 9 #include <stdio.h> 10 11 #include <emscripten/html5.h> 12 13 #ifdef __cplusplus 14 # define PUGL_INIT_STRUCT \ 15 {} 16 #else 17 # define PUGL_INIT_STRUCT \ 18 { \ 19 0 \ 20 } 21 #endif 22 23 #ifdef __MOD_DEVICES__ 24 # define MOD_SCALE_FACTOR_MULT 1 25 #endif 26 27 // #define PUGL_WASM_AUTO_POINTER_LOCK 28 // #define PUGL_WASM_NO_KEYBOARD_INPUT 29 // #define PUGL_WASM_NO_MOUSEWHEEL_INPUT 30 31 PuglWorldInternals* 32 puglInitWorldInternals(const PuglWorldType type, const PuglWorldFlags flags) 33 { 34 PuglWorldInternals* impl = 35 (PuglWorldInternals*)calloc(1, sizeof(PuglWorldInternals)); 36 37 impl->scaleFactor = emscripten_get_device_pixel_ratio(); 38 #ifdef __MOD_DEVICES__ 39 impl->scaleFactor *= MOD_SCALE_FACTOR_MULT; 40 #endif 41 42 printf("DONE: %s %d | -> %f\n", __func__, __LINE__, impl->scaleFactor); 43 44 return impl; 45 } 46 47 void* 48 puglGetNativeWorld(PuglWorld*) 49 { 50 printf("DONE: %s %d\n", __func__, __LINE__); 51 return NULL; 52 } 53 54 PuglInternals* 55 puglInitViewInternals(PuglWorld* const world) 56 { 57 printf("DONE: %s %d\n", __func__, __LINE__); 58 PuglInternals* impl = (PuglInternals*)calloc(1, sizeof(PuglInternals)); 59 60 impl->buttonPressTimeout = -1; 61 impl->supportsTouch = PUGL_DONT_CARE; // not yet known 62 63 #ifdef PUGL_WASM_ASYNC_CLIPBOARD 64 impl->supportsClipboardRead = (PuglViewHintValue)EM_ASM_INT({ 65 if (typeof(navigator.clipboard) !== 'undefined' && typeof(navigator.clipboard.readText) === 'function' && window.isSecureContext) { 66 return 1; // PUGL_TRUE 67 } 68 return 0; // PUGL_FALSE 69 }); 70 71 impl->supportsClipboardWrite = (PuglViewHintValue)EM_ASM_INT({ 72 if (typeof(navigator.clipboard) !== 'undefined' && typeof(navigator.clipboard.writeText) === 'function' && window.isSecureContext) { 73 return 1; // PUGL_TRUE 74 } 75 if (typeof(document.queryCommandSupported) !== 'undefined' && document.queryCommandSupported("copy")) { 76 return 1; // PUGL_TRUE 77 } 78 return 0; // PUGL_FALSE 79 }); 80 #endif 81 82 return impl; 83 } 84 85 static PuglStatus 86 puglDispatchEventWithContext(PuglView* const view, const PuglEvent* event) 87 { 88 PuglStatus st0 = PUGL_SUCCESS; 89 PuglStatus st1 = PUGL_SUCCESS; 90 91 if (!(st0 = view->backend->enter(view, NULL))) { 92 st0 = view->eventFunc(view, event); 93 st1 = view->backend->leave(view, NULL); 94 } 95 96 return st0 ? st0 : st1; 97 } 98 99 static PuglMods 100 translateModifiers(const EM_BOOL ctrlKey, 101 const EM_BOOL shiftKey, 102 const EM_BOOL altKey, 103 const EM_BOOL metaKey) 104 { 105 return (ctrlKey ? PUGL_MOD_CTRL : 0u) | 106 (shiftKey ? PUGL_MOD_SHIFT : 0u) | 107 (altKey ? PUGL_MOD_ALT : 0u) | 108 (metaKey ? PUGL_MOD_SUPER : 0u); 109 } 110 111 #ifndef PUGL_WASM_NO_KEYBOARD_INPUT 112 static PuglKey 113 keyCodeToSpecial(const unsigned long code, const unsigned long location) 114 { 115 switch (code) { 116 case 0x08: return PUGL_KEY_BACKSPACE; 117 case 0x1B: return PUGL_KEY_ESCAPE; 118 case 0x2E: return PUGL_KEY_DELETE; 119 case 0x70: return PUGL_KEY_F1; 120 case 0x71: return PUGL_KEY_F2; 121 case 0x72: return PUGL_KEY_F3; 122 case 0x73: return PUGL_KEY_F4; 123 case 0x74: return PUGL_KEY_F5; 124 case 0x75: return PUGL_KEY_F6; 125 case 0x76: return PUGL_KEY_F7; 126 case 0x77: return PUGL_KEY_F8; 127 case 0x78: return PUGL_KEY_F9; 128 case 0x79: return PUGL_KEY_F10; 129 case 0x7A: return PUGL_KEY_F11; 130 case 0x7B: return PUGL_KEY_F12; 131 case 0x25: return PUGL_KEY_LEFT; 132 case 0x26: return PUGL_KEY_UP; 133 case 0x27: return PUGL_KEY_RIGHT; 134 case 0x28: return PUGL_KEY_DOWN; 135 case 0x21: return PUGL_KEY_PAGE_UP; 136 case 0x22: return PUGL_KEY_PAGE_DOWN; 137 case 0x24: return PUGL_KEY_HOME; 138 case 0x23: return PUGL_KEY_END; 139 case 0x2D: return PUGL_KEY_INSERT; 140 case 0x10: return location == DOM_KEY_LOCATION_RIGHT ? PUGL_KEY_SHIFT_R : PUGL_KEY_SHIFT_L; 141 case 0x11: return location == DOM_KEY_LOCATION_RIGHT ? PUGL_KEY_CTRL_R : PUGL_KEY_CTRL_L; 142 case 0x12: return location == DOM_KEY_LOCATION_RIGHT ? PUGL_KEY_ALT_R : PUGL_KEY_ALT_L; 143 case 0xE0: return location == DOM_KEY_LOCATION_RIGHT ? PUGL_KEY_SUPER_R : PUGL_KEY_SUPER_L; 144 case 0x5D: return PUGL_KEY_MENU; 145 case 0x14: return PUGL_KEY_CAPS_LOCK; 146 case 0x91: return PUGL_KEY_SCROLL_LOCK; 147 case 0x90: return PUGL_KEY_NUM_LOCK; 148 case 0x2C: return PUGL_KEY_PRINT_SCREEN; 149 case 0x13: return PUGL_KEY_PAUSE; 150 case '\r': return (PuglKey)'\r'; 151 default: break; 152 } 153 154 return (PuglKey)0; 155 } 156 157 static bool 158 decodeCharacterString(const unsigned long keyCode, 159 const EM_UTF8 key[EM_HTML5_SHORT_STRING_LEN_BYTES], 160 char str[8]) 161 { 162 if (key[1] == 0) 163 { 164 str[0] = key[0]; 165 return true; 166 } 167 168 return false; 169 } 170 171 static EM_BOOL 172 puglKeyCallback(const int eventType, const EmscriptenKeyboardEvent* const keyEvent, void* const userData) 173 { 174 PuglView* const view = (PuglView*)userData; 175 176 if (!view->impl->visible) { 177 return EM_FALSE; 178 } 179 180 if (keyEvent->repeat && view->hints[PUGL_IGNORE_KEY_REPEAT]) 181 return EM_TRUE; 182 183 PuglStatus st0 = PUGL_SUCCESS; 184 PuglStatus st1 = PUGL_SUCCESS; 185 186 const uint state = translateModifiers(keyEvent->ctrlKey, 187 keyEvent->shiftKey, 188 keyEvent->altKey, 189 keyEvent->metaKey); 190 191 const PuglKey special = keyCodeToSpecial(keyEvent->keyCode, keyEvent->location); 192 193 uint key = keyEvent->key[0] >= ' ' && keyEvent->key[0] <= '~' && keyEvent->key[1] == '\0' 194 ? keyEvent->key[0] 195 : keyEvent->keyCode; 196 197 if (key >= 'A' && key <= 'Z' && !keyEvent->shiftKey) 198 key += 'a' - 'A'; 199 200 PuglEvent event = {{PUGL_NOTHING, 0}}; 201 event.key.type = eventType == EMSCRIPTEN_EVENT_KEYDOWN ? PUGL_KEY_PRESS : PUGL_KEY_RELEASE; 202 event.key.time = keyEvent->timestamp / 1e3; 203 // event.key.x = xevent.xkey.x; 204 // event.key.y = xevent.xkey.y; 205 // event.key.xRoot = xevent.xkey.x_root; 206 // event.key.yRoot = xevent.xkey.y_root; 207 event.key.key = special ? special : key; 208 event.key.keycode = keyEvent->keyCode; 209 event.key.state = state; 210 st0 = puglDispatchEventWithContext(view, &event); 211 212 d_debug("key event \n" 213 "\tdown: %d\n" 214 "\trepeat: %d\n" 215 "\tlocation: %d\n" 216 "\tstate: 0x%x\n" 217 "\tkey[]: '%s'\n" 218 "\tcode[]: '%s'\n" 219 "\tlocale[]: '%s'\n" 220 "\tkeyCode: 0x%lx:'%c' [deprecated, use key]\n" 221 "\twhich: 0x%lx:'%c' [deprecated, use key, same as keycode?]\n" 222 "\tspecial: 0x%x", 223 eventType == EMSCRIPTEN_EVENT_KEYDOWN, 224 keyEvent->repeat, 225 keyEvent->location, 226 state, 227 keyEvent->key, 228 keyEvent->code, 229 keyEvent->locale, 230 keyEvent->keyCode, keyEvent->keyCode >= ' ' && keyEvent->keyCode <= '~' ? keyEvent->keyCode : 0, 231 keyEvent->which, keyEvent->which >= ' ' && keyEvent->which <= '~' ? keyEvent->which : 0, 232 special); 233 234 if (event.type == PUGL_KEY_PRESS && !special && !(keyEvent->ctrlKey|keyEvent->altKey|keyEvent->metaKey)) { 235 char str[8] = PUGL_INIT_STRUCT; 236 237 if (decodeCharacterString(keyEvent->keyCode, keyEvent->key, str)) { 238 d_debug("resulting string is '%s'", str); 239 240 event.text.type = PUGL_TEXT; 241 event.text.character = event.key.key; 242 memcpy(event.text.string, str, sizeof(event.text.string)); 243 st1 = puglDispatchEventWithContext(view, &event); 244 } 245 } 246 247 return (st0 ? st0 : st1) == PUGL_SUCCESS ? EM_TRUE : EM_FALSE; 248 } 249 #endif 250 251 static EM_BOOL 252 puglMouseCallback(const int eventType, const EmscriptenMouseEvent* const mouseEvent, void* const userData) 253 { 254 PuglView* const view = (PuglView*)userData; 255 256 if (!view->impl->visible) { 257 return EM_FALSE; 258 } 259 260 PuglEvent event = {{PUGL_NOTHING, 0}}; 261 262 const double time = mouseEvent->timestamp / 1e3; 263 const PuglMods state = translateModifiers(mouseEvent->ctrlKey, 264 mouseEvent->shiftKey, 265 mouseEvent->altKey, 266 mouseEvent->metaKey); 267 268 double scaleFactor = view->world->impl->scaleFactor; 269 #ifdef __MOD_DEVICES__ 270 if (!view->impl->isFullscreen) { 271 scaleFactor /= EM_ASM_DOUBLE({ 272 return parseFloat( 273 RegExp('^scale\\\((.*)\\\)$') 274 .exec(document.getElementById("pedalboard-dashboard").style.transform)[1] 275 ); 276 }) * MOD_SCALE_FACTOR_MULT; 277 } 278 #endif 279 280 // workaround missing pointer lock callback, see https://github.com/emscripten-core/emscripten/issues/9681 281 EmscriptenPointerlockChangeEvent e; 282 if (emscripten_get_pointerlock_status(&e) == EMSCRIPTEN_RESULT_SUCCESS) 283 view->impl->pointerLocked = e.isActive; 284 285 #ifdef __MOD_DEVICES__ 286 const long canvasX = mouseEvent->canvasX; 287 const long canvasY = mouseEvent->canvasY; 288 #else 289 const char* const className = view->world->strings[PUGL_CLASS_NAME]; 290 const double canvasX = mouseEvent->clientX - EM_ASM_DOUBLE({ 291 var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement; 292 return canvasWrapper.getBoundingClientRect().x; 293 }, className); 294 const double canvasY = mouseEvent->clientY - EM_ASM_DOUBLE({ 295 var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement; 296 return canvasWrapper.getBoundingClientRect().y; 297 }, className); 298 #endif 299 300 switch (eventType) { 301 case EMSCRIPTEN_EVENT_MOUSEDOWN: 302 case EMSCRIPTEN_EVENT_MOUSEUP: 303 event.button.type = eventType == EMSCRIPTEN_EVENT_MOUSEDOWN ? PUGL_BUTTON_PRESS : PUGL_BUTTON_RELEASE; 304 event.button.time = time; 305 event.button.x = canvasX * scaleFactor; 306 event.button.y = canvasY * scaleFactor; 307 event.button.xRoot = mouseEvent->screenX * scaleFactor; 308 event.button.yRoot = mouseEvent->screenY * scaleFactor; 309 event.button.state = state; 310 switch (mouseEvent->button) { 311 case 1: 312 event.button.button = 2; 313 break; 314 case 2: 315 event.button.button = 1; 316 break; 317 default: 318 event.button.button = mouseEvent->button; 319 break; 320 } 321 break; 322 case EMSCRIPTEN_EVENT_MOUSEMOVE: 323 event.motion.type = PUGL_MOTION; 324 event.motion.time = time; 325 if (view->impl->pointerLocked) { 326 // adjust local values for delta 327 const double movementX = mouseEvent->movementX * scaleFactor; 328 const double movementY = mouseEvent->movementY * scaleFactor; 329 view->impl->lastMotion.x += movementX; 330 view->impl->lastMotion.y += movementY; 331 view->impl->lastMotion.xRoot += movementX; 332 view->impl->lastMotion.yRoot += movementY; 333 // now set x, y, xRoot and yRoot 334 event.motion.x = view->impl->lastMotion.x; 335 event.motion.y = view->impl->lastMotion.y; 336 event.motion.xRoot = view->impl->lastMotion.xRoot; 337 event.motion.yRoot = view->impl->lastMotion.yRoot; 338 } else { 339 // cache values for possible pointer lock movement later 340 view->impl->lastMotion.x = event.motion.x = canvasX * scaleFactor; 341 view->impl->lastMotion.y = event.motion.y = canvasY * scaleFactor; 342 view->impl->lastMotion.xRoot = event.motion.xRoot = mouseEvent->screenX * scaleFactor; 343 view->impl->lastMotion.yRoot = event.motion.yRoot = mouseEvent->screenY * scaleFactor; 344 } 345 event.motion.state = state; 346 break; 347 case EMSCRIPTEN_EVENT_MOUSEENTER: 348 case EMSCRIPTEN_EVENT_MOUSELEAVE: 349 event.crossing.type = eventType == EMSCRIPTEN_EVENT_MOUSEENTER ? PUGL_POINTER_IN : PUGL_POINTER_OUT; 350 event.crossing.time = time; 351 event.crossing.x = canvasX * scaleFactor; 352 event.crossing.y = canvasY * scaleFactor; 353 event.crossing.xRoot = mouseEvent->screenX * scaleFactor; 354 event.crossing.yRoot = mouseEvent->screenY * scaleFactor; 355 event.crossing.state = state; 356 event.crossing.mode = PUGL_CROSSING_NORMAL; 357 break; 358 } 359 360 if (event.type == PUGL_NOTHING) 361 return EM_FALSE; 362 363 puglDispatchEventWithContext(view, &event); 364 365 #ifdef PUGL_WASM_AUTO_POINTER_LOCK 366 switch (eventType) { 367 case EMSCRIPTEN_EVENT_MOUSEDOWN: 368 emscripten_request_pointerlock(view->world->strings[PUGL_CLASS_NAME], false); 369 break; 370 case EMSCRIPTEN_EVENT_MOUSEUP: 371 emscripten_exit_pointerlock(); 372 break; 373 } 374 #endif 375 376 // note: we must always return false, otherwise canvas never gets keyboard input 377 return EM_FALSE; 378 } 379 380 static void 381 puglTouchStartDelay(void* const userData) 382 { 383 PuglView* const view = (PuglView*)userData; 384 PuglInternals* const impl = view->impl; 385 386 impl->buttonPressTimeout = -1; 387 impl->nextButtonEvent.button.time += 2000; 388 puglDispatchEventWithContext(view, &impl->nextButtonEvent); 389 } 390 391 static EM_BOOL 392 puglTouchCallback(const int eventType, const EmscriptenTouchEvent* const touchEvent, void* const userData) 393 { 394 if (touchEvent->numTouches <= 0) { 395 return EM_FALSE; 396 } 397 398 PuglView* const view = (PuglView*)userData; 399 PuglInternals* const impl = view->impl; 400 const char* const className = view->world->strings[PUGL_CLASS_NAME]; 401 402 if (impl->supportsTouch == PUGL_DONT_CARE) { 403 impl->supportsTouch = PUGL_TRUE; 404 405 // stop using mouse press events which conflict with touch 406 emscripten_set_mousedown_callback(className, view, false, NULL); 407 emscripten_set_mouseup_callback(className, view, false, NULL); 408 } 409 410 if (!view->impl->visible) { 411 return EM_FALSE; 412 } 413 414 PuglEvent event = {{PUGL_NOTHING, 0}}; 415 416 const double time = touchEvent->timestamp / 1e3; 417 const PuglMods state = translateModifiers(touchEvent->ctrlKey, 418 touchEvent->shiftKey, 419 touchEvent->altKey, 420 touchEvent->metaKey); 421 422 double scaleFactor = view->world->impl->scaleFactor; 423 #ifdef __MOD_DEVICES__ 424 if (!view->impl->isFullscreen) { 425 scaleFactor /= EM_ASM_DOUBLE({ 426 return parseFloat( 427 RegExp('^scale\\\((.*)\\\)$') 428 .exec(document.getElementById("pedalboard-dashboard").style.transform)[1] 429 ); 430 }) * MOD_SCALE_FACTOR_MULT; 431 } 432 #endif 433 434 d_debug("touch %d|%s %d || %ld", 435 eventType, 436 eventType == EMSCRIPTEN_EVENT_TOUCHSTART ? "start" : 437 eventType == EMSCRIPTEN_EVENT_TOUCHEND ? "end" : "cancel", 438 touchEvent->numTouches, 439 impl->buttonPressTimeout); 440 441 const EmscriptenTouchPoint* point = &touchEvent->touches[0]; 442 443 if (impl->buttonPressTimeout != -1 || eventType == EMSCRIPTEN_EVENT_TOUCHCANCEL) { 444 // if we received an event while touch is active, trigger initial click now 445 if (impl->buttonPressTimeout != -1) { 446 emscripten_clear_timeout(impl->buttonPressTimeout); 447 impl->buttonPressTimeout = -1; 448 if (eventType != EMSCRIPTEN_EVENT_TOUCHCANCEL) { 449 impl->nextButtonEvent.button.button = 0; 450 } 451 } 452 impl->nextButtonEvent.button.time = time; 453 puglDispatchEventWithContext(view, &impl->nextButtonEvent); 454 } 455 456 #ifdef __MOD_DEVICES__ 457 const long canvasX = point->canvasX; 458 const long canvasY = point->canvasY; 459 #else 460 const double canvasX = point->clientX - EM_ASM_DOUBLE({ 461 var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement; 462 return canvasWrapper.getBoundingClientRect().x; 463 }, className); 464 const double canvasY = point->clientY - EM_ASM_DOUBLE({ 465 var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement; 466 return canvasWrapper.getBoundingClientRect().y; 467 }, className); 468 #endif 469 470 switch (eventType) { 471 case EMSCRIPTEN_EVENT_TOUCHEND: 472 case EMSCRIPTEN_EVENT_TOUCHCANCEL: 473 event.button.type = PUGL_BUTTON_RELEASE; 474 event.button.time = time; 475 event.button.button = eventType == EMSCRIPTEN_EVENT_TOUCHCANCEL ? 1 : 0; 476 event.button.x = canvasX * scaleFactor; 477 event.button.y = canvasY * scaleFactor; 478 event.button.xRoot = point->screenX * scaleFactor; 479 event.button.yRoot = point->screenY * scaleFactor; 480 event.button.state = state; 481 break; 482 483 case EMSCRIPTEN_EVENT_TOUCHSTART: 484 // this event can be used for a couple of things, store it until we know more 485 event.button.type = PUGL_BUTTON_PRESS; 486 event.button.time = time; 487 event.button.button = 1; // if no other event occurs soon, treat it as right-click 488 event.button.x = canvasX * scaleFactor; 489 event.button.y = canvasY * scaleFactor; 490 event.button.xRoot = point->screenX * scaleFactor; 491 event.button.yRoot = point->screenY * scaleFactor; 492 event.button.state = state; 493 memcpy(&impl->nextButtonEvent, &event, sizeof(PuglEvent)); 494 impl->buttonPressTimeout = emscripten_set_timeout(puglTouchStartDelay, 2000, view); 495 // fall through, moving "mouse" to touch position 496 497 case EMSCRIPTEN_EVENT_TOUCHMOVE: 498 event.motion.type = PUGL_MOTION; 499 event.motion.time = time; 500 event.motion.x = canvasX * scaleFactor; 501 event.motion.y = canvasY * scaleFactor; 502 event.motion.xRoot = point->screenX * scaleFactor; 503 event.motion.yRoot = point->screenY * scaleFactor; 504 event.motion.state = state; 505 break; 506 } 507 508 if (event.type == PUGL_NOTHING) 509 return EM_FALSE; 510 511 puglDispatchEventWithContext(view, &event); 512 513 // FIXME we must always return false?? 514 return EM_FALSE; 515 } 516 517 static EM_BOOL 518 puglFocusCallback(const int eventType, const EmscriptenFocusEvent* /*const focusEvent*/, void* const userData) 519 { 520 PuglView* const view = (PuglView*)userData; 521 522 if (!view->impl->visible) { 523 return EM_FALSE; 524 } 525 526 d_debug("focus %d|%s", eventType, eventType == EMSCRIPTEN_EVENT_FOCUSIN ? "focus-in" : "focus-out"); 527 528 PuglEvent event = {{eventType == EMSCRIPTEN_EVENT_FOCUSIN ? PUGL_FOCUS_IN : PUGL_FOCUS_OUT, 0}}; 529 event.focus.mode = PUGL_CROSSING_NORMAL; 530 531 puglDispatchEventWithContext(view, &event); 532 533 // note: we must always return false, otherwise canvas never gets proper focus 534 return EM_FALSE; 535 } 536 537 static EM_BOOL 538 puglPointerLockChangeCallback(const int eventType, const EmscriptenPointerlockChangeEvent* event, void* const userData) 539 { 540 PuglView* const view = (PuglView*)userData; 541 542 view->impl->pointerLocked = event->isActive; 543 return EM_TRUE; 544 } 545 546 #ifndef PUGL_WASM_NO_MOUSEWHEEL_INPUT 547 static EM_BOOL 548 puglWheelCallback(const int eventType, const EmscriptenWheelEvent* const wheelEvent, void* const userData) 549 { 550 PuglView* const view = (PuglView*)userData; 551 552 if (!view->impl->visible) { 553 return EM_FALSE; 554 } 555 556 double scaleFactor = view->world->impl->scaleFactor; 557 #ifdef __MOD_DEVICES__ 558 if (!view->impl->isFullscreen) { 559 scaleFactor /= EM_ASM_DOUBLE({ 560 return parseFloat( 561 RegExp('^scale\\\((.*)\\\)$') 562 .exec(document.getElementById("pedalboard-dashboard").style.transform)[1] 563 ); 564 }) * MOD_SCALE_FACTOR_MULT; 565 } 566 #endif 567 568 #ifdef __MOD_DEVICES__ 569 const long canvasX = wheelEvent->mouse.canvasX; 570 const long canvasY = wheelEvent->mouse.canvasY; 571 #else 572 const char* const className = view->world->strings[PUGL_CLASS_NAME]; 573 const double canvasX = wheelEvent->mouse.canvasX - EM_ASM_INT({ 574 var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement; 575 return canvasWrapper.getBoundingClientRect().x; 576 }, className); 577 const double canvasY = wheelEvent->mouse.canvasY - EM_ASM_INT({ 578 var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement; 579 return canvasWrapper.getBoundingClientRect().y; 580 }, className); 581 #endif 582 583 PuglEvent event = {{PUGL_SCROLL, 0}}; 584 event.scroll.time = wheelEvent->mouse.timestamp / 1e3; 585 event.scroll.x = canvasX; 586 event.scroll.y = canvasY; 587 event.scroll.xRoot = wheelEvent->mouse.screenX; 588 event.scroll.yRoot = wheelEvent->mouse.screenY; 589 event.scroll.state = translateModifiers(wheelEvent->mouse.ctrlKey, 590 wheelEvent->mouse.shiftKey, 591 wheelEvent->mouse.altKey, 592 wheelEvent->mouse.metaKey); 593 event.scroll.direction = PUGL_SCROLL_SMOOTH; 594 // FIXME handle wheelEvent->deltaMode 595 event.scroll.dx = wheelEvent->deltaX * 0.01 * scaleFactor; 596 event.scroll.dy = -wheelEvent->deltaY * 0.01 * scaleFactor; 597 598 return puglDispatchEventWithContext(view, &event) == PUGL_SUCCESS ? EM_TRUE : EM_FALSE; 599 } 600 #endif 601 602 static EM_BOOL 603 puglUiCallback(const int eventType, const EmscriptenUiEvent* const uiEvent, void* const userData) 604 { 605 PuglView* const view = (PuglView*)userData; 606 const char* const className = view->world->strings[PUGL_CLASS_NAME]; 607 const bool isFullscreen = view->impl->isFullscreen; 608 609 // FIXME 610 const int width = EM_ASM_INT({ 611 var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement; 612 canvasWrapper.style.setProperty("--device-pixel-ratio", window.devicePixelRatio); 613 if ($1) { 614 return window.innerWidth; 615 } else { 616 return canvasWrapper.clientWidth; 617 } 618 }, className, isFullscreen); 619 620 const int height = EM_ASM_INT({ 621 if ($1) { 622 return window.innerHeight; 623 } else { 624 var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement; 625 return canvasWrapper.clientHeight; 626 } 627 }, className, isFullscreen); 628 629 if (!width || !height) 630 return EM_FALSE; 631 632 double scaleFactor = emscripten_get_device_pixel_ratio(); 633 #ifdef __MOD_DEVICES__ 634 scaleFactor *= MOD_SCALE_FACTOR_MULT; 635 #endif 636 view->world->impl->scaleFactor = scaleFactor; 637 638 PuglEvent event = {{PUGL_CONFIGURE, 0}}; 639 event.configure.x = view->lastConfigure.x; 640 event.configure.y = view->lastConfigure.y; 641 event.configure.width = width * scaleFactor; 642 event.configure.height = height * scaleFactor; 643 puglDispatchEvent(view, &event); 644 645 emscripten_set_canvas_element_size(view->world->strings[PUGL_CLASS_NAME], 646 width * scaleFactor, 647 height * scaleFactor); 648 649 #ifdef __MOD_DEVICES__ 650 if (isFullscreen) { 651 EM_ASM({ 652 document.getElementById("pedalboard-dashboard").style.transform = "scale(1.0)"; 653 }); 654 } 655 #endif 656 657 return EM_TRUE; 658 } 659 660 static EM_BOOL 661 puglFullscreenChangeCallback(const int eventType, const EmscriptenFullscreenChangeEvent* const fscEvent, void* const userData) 662 { 663 PuglView* const view = (PuglView*)userData; 664 665 view->impl->isFullscreen = fscEvent->isFullscreen; 666 667 double scaleFactor = emscripten_get_device_pixel_ratio(); 668 #ifdef __MOD_DEVICES__ 669 scaleFactor *= MOD_SCALE_FACTOR_MULT; 670 #endif 671 672 view->world->impl->scaleFactor = scaleFactor; 673 674 if (!fscEvent->isFullscreen) 675 return puglUiCallback(0, NULL, userData); 676 677 const int width = EM_ASM_INT({ 678 return window.innerWidth; 679 }); 680 681 const int height = EM_ASM_INT({ 682 return window.innerHeight; 683 }); 684 685 PuglEvent event = {{PUGL_CONFIGURE, 0}}; 686 event.configure.x = 0; 687 event.configure.y = 0; 688 event.configure.width = width; 689 event.configure.height = height; 690 puglDispatchEvent(view, &event); 691 692 emscripten_set_canvas_element_size(view->world->strings[PUGL_CLASS_NAME], width, height); 693 694 #ifdef __MOD_DEVICES__ 695 EM_ASM({ 696 document.getElementById("pedalboard-dashboard").style.transform = "scale(1.0)"; 697 }); 698 #endif 699 700 return EM_TRUE; 701 } 702 703 static EM_BOOL 704 puglVisibilityChangeCallback(const int eventType, const EmscriptenVisibilityChangeEvent* const visibilityChangeEvent, void* const userData) 705 { 706 PuglView* const view = (PuglView*)userData; 707 708 view->impl->visible = visibilityChangeEvent->hidden == EM_FALSE; 709 return EM_FALSE; 710 } 711 712 PuglStatus 713 puglRealize(PuglView* const view) 714 { 715 printf("TODO: %s %d\n", __func__, __LINE__); 716 PuglStatus st = PUGL_SUCCESS; 717 718 // Ensure that we do not have a parent 719 if (view->parent) { 720 printf("TODO: %s %d\n", __func__, __LINE__); 721 return PUGL_FAILURE; 722 } 723 724 if (!view->backend || !view->backend->configure) { 725 printf("TODO: %s %d\n", __func__, __LINE__); 726 return PUGL_BAD_BACKEND; 727 } 728 729 const char* const className = view->world->strings[PUGL_CLASS_NAME]; 730 d_stdout("className is %s", className); 731 732 PuglViewSize defaultSize = view->sizeHints[PUGL_DEFAULT_SIZE]; 733 if (!defaultSize.width || !defaultSize.height) { 734 return PUGL_BAD_CONFIGURATION; 735 } 736 737 // Configure and create the backend 738 if ((st = view->backend->configure(view)) || (st = view->backend->create(view))) { 739 view->backend->destroy(view); 740 return st; 741 } 742 743 if (view->strings[PUGL_WINDOW_TITLE]) { 744 emscripten_set_window_title(view->strings[PUGL_WINDOW_TITLE]); 745 } 746 747 puglDispatchSimpleEvent(view, PUGL_REALIZE); 748 749 PuglEvent event = {{PUGL_CONFIGURE, 0}}; 750 event.configure.x = view->defaultX; 751 event.configure.y = view->defaultY; 752 event.configure.width = defaultSize.width; 753 event.configure.height = defaultSize.height; 754 puglDispatchEvent(view, &event); 755 756 view->impl->created = true; 757 758 EM_ASM({ 759 var canvasWrapper = document.getElementById(UTF8ToString($0)).parentElement; 760 canvasWrapper.style.setProperty("--device-pixel-ratio", window.devicePixelRatio); 761 }, className); 762 763 emscripten_set_canvas_element_size(className, defaultSize.width, defaultSize.height); 764 #ifndef PUGL_WASM_NO_KEYBOARD_INPUT 765 // emscripten_set_keypress_callback(className, view, false, puglKeyCallback); 766 emscripten_set_keydown_callback(className, view, false, puglKeyCallback); 767 emscripten_set_keyup_callback(className, view, false, puglKeyCallback); 768 #endif 769 emscripten_set_touchstart_callback(className, view, false, puglTouchCallback); 770 emscripten_set_touchend_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglTouchCallback); 771 emscripten_set_touchmove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglTouchCallback); 772 emscripten_set_touchcancel_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglTouchCallback); 773 emscripten_set_mousedown_callback(className, view, false, puglMouseCallback); 774 emscripten_set_mouseup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglMouseCallback); 775 emscripten_set_mousemove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglMouseCallback); 776 emscripten_set_mouseenter_callback(className, view, false, puglMouseCallback); 777 emscripten_set_mouseleave_callback(className, view, false, puglMouseCallback); 778 emscripten_set_focusin_callback(className, view, false, puglFocusCallback); 779 emscripten_set_focusout_callback(className, view, false, puglFocusCallback); 780 #ifndef PUGL_WASM_NO_MOUSEWHEEL_INPUT 781 emscripten_set_wheel_callback(className, view, false, puglWheelCallback); 782 #endif 783 emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglPointerLockChangeCallback); 784 emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglUiCallback); 785 emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, view, false, puglFullscreenChangeCallback); 786 emscripten_set_visibilitychange_callback(view, false, puglVisibilityChangeCallback); 787 788 printf("TODO: %s %d\n", __func__, __LINE__); 789 return PUGL_SUCCESS; 790 } 791 792 PuglStatus 793 puglShow(PuglView* const view, PuglShowCommand) 794 { 795 view->impl->visible = true; 796 view->impl->needsRepaint = true; 797 return puglPostRedisplay(view); 798 } 799 800 PuglStatus 801 puglHide(PuglView* const view) 802 { 803 view->impl->visible = false; 804 return PUGL_FAILURE; 805 } 806 807 void 808 puglFreeViewInternals(PuglView* const view) 809 { 810 printf("DONE: %s %d\n", __func__, __LINE__); 811 if (view && view->impl) { 812 if (view->backend) { 813 // unregister the window events, to make sure no callbacks to old views are triggered 814 emscripten_set_touchend_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL); 815 emscripten_set_touchmove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL); 816 emscripten_set_touchcancel_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL); 817 emscripten_set_mouseup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL); 818 emscripten_set_mousemove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL); 819 emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL); 820 emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL); 821 emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, NULL, false, NULL); 822 emscripten_set_visibilitychange_callback(NULL, false, NULL); 823 view->backend->destroy(view); 824 } 825 free(view->impl->clipboardData); 826 free(view->impl->timers); 827 free(view->impl); 828 } 829 } 830 831 void 832 puglFreeWorldInternals(PuglWorld* const world) 833 { 834 printf("DONE: %s %d\n", __func__, __LINE__); 835 free(world->impl); 836 } 837 838 PuglStatus 839 puglGrabFocus(PuglView*) 840 { 841 return PUGL_FAILURE; 842 } 843 844 double 845 puglGetScaleFactor(const PuglView* const view) 846 { 847 printf("DONE: %s %d\n", __func__, __LINE__); 848 return view->world->impl->scaleFactor; 849 } 850 851 double 852 puglGetTime(const PuglWorld*) 853 { 854 return emscripten_get_now() / 1e3; 855 } 856 857 PuglStatus 858 puglUpdate(PuglWorld* const world, const double timeout) 859 { 860 for (size_t i = 0; i < world->numViews; ++i) { 861 PuglView* const view = world->views[i]; 862 863 if (!view->impl->visible) { 864 continue; 865 } 866 867 puglDispatchSimpleEvent(view, PUGL_UPDATE); 868 869 if (!view->impl->needsRepaint) { 870 continue; 871 } 872 873 view->impl->needsRepaint = false; 874 875 PuglEvent event = {{PUGL_EXPOSE, 0}}; 876 event.expose.x = view->lastConfigure.x; 877 event.expose.y = view->lastConfigure.y; 878 event.expose.width = view->lastConfigure.width; 879 event.expose.height = view->lastConfigure.height; 880 puglDispatchEvent(view, &event); 881 } 882 883 return PUGL_SUCCESS; 884 } 885 886 PuglStatus 887 puglPostRedisplay(PuglView* const view) 888 { 889 view->impl->needsRepaint = true; 890 return PUGL_SUCCESS; 891 } 892 893 PuglStatus 894 puglPostRedisplayRect(PuglView* const view, const PuglRect rect) 895 { 896 view->impl->needsRepaint = true; 897 return PUGL_FAILURE; 898 } 899 900 PuglNativeView 901 puglGetNativeView(PuglView* const view) 902 { 903 return 0; 904 } 905 906 PuglStatus 907 puglViewStringChanged(PuglView*, const PuglStringHint key, const char* const value) 908 { 909 switch (key) { 910 case PUGL_CLASS_NAME: 911 break; 912 case PUGL_WINDOW_TITLE: 913 emscripten_set_window_title(value); 914 break; 915 } 916 917 return PUGL_SUCCESS; 918 } 919 920 PuglStatus 921 puglSetSizeHint(PuglView* const view, 922 const PuglSizeHint hint, 923 const PuglSpan width, 924 const PuglSpan height) 925 { 926 view->sizeHints[hint].width = width; 927 view->sizeHints[hint].height = height; 928 return PUGL_SUCCESS; 929 } 930 931 static EM_BOOL 932 puglTimerLoopCallback(double timeout, void* const arg) 933 { 934 PuglTimer* const timer = (PuglTimer*)arg; 935 PuglInternals* const impl = timer->view->impl; 936 937 // only handle active timers 938 for (uint32_t i=0; i<impl->numTimers; ++i) 939 { 940 if (impl->timers[i].id == timer->id) 941 { 942 PuglEvent event = {{PUGL_TIMER, 0}}; 943 event.timer.id = timer->id; 944 puglDispatchEventWithContext(timer->view, &event); 945 return EM_TRUE; 946 } 947 } 948 949 return EM_FALSE; 950 951 // unused 952 (void)timeout; 953 } 954 955 PuglStatus 956 puglStartTimer(PuglView* const view, const uintptr_t id, const double timeout) 957 { 958 printf("DONE: %s %d\n", __func__, __LINE__); 959 PuglInternals* const impl = view->impl; 960 const uint32_t timerIndex = impl->numTimers++; 961 962 if (impl->timers == NULL) 963 impl->timers = (PuglTimer*)malloc(sizeof(PuglTimer)); 964 else 965 impl->timers = (PuglTimer*)realloc(impl->timers, sizeof(PuglTimer) * timerIndex); 966 967 PuglTimer* const timer = &impl->timers[timerIndex]; 968 timer->view = view; 969 timer->id = id; 970 971 emscripten_set_timeout_loop(puglTimerLoopCallback, timeout * 1e3, timer); 972 return PUGL_SUCCESS; 973 } 974 975 PuglStatus 976 puglStopTimer(PuglView* const view, const uintptr_t id) 977 { 978 printf("DONE: %s %d\n", __func__, __LINE__); 979 PuglInternals* const impl = view->impl; 980 981 if (impl->timers == NULL || impl->numTimers == 0) 982 return PUGL_FAILURE; 983 984 for (uint32_t i=0; i<impl->numTimers; ++i) 985 { 986 if (impl->timers[i].id == id) 987 { 988 memmove(impl->timers + i, impl->timers + (i + 1), sizeof(PuglTimer) * (impl->numTimers - 1)); 989 --impl->numTimers; 990 return PUGL_SUCCESS; 991 } 992 } 993 994 return PUGL_FAILURE; 995 } 996 997 #ifdef PUGL_WASM_ASYNC_CLIPBOARD 998 EM_JS(char*, puglGetAsyncClipboardData, (), { 999 var text = Asyncify.handleSleep(function(wakeUp) { 1000 navigator.clipboard.readText() 1001 .then(function(text) { 1002 wakeUp(text); 1003 }) 1004 .catch(function() { 1005 wakeUp(""); 1006 }); 1007 }); 1008 if (!text.length) { 1009 return null; 1010 } 1011 var length = lengthBytesUTF8(text) + 1; 1012 var str = _malloc(length); 1013 stringToUTF8(text, str, length); 1014 return str; 1015 }); 1016 #endif 1017 1018 PuglStatus 1019 puglPaste(PuglView* const view) 1020 { 1021 #ifdef PUGL_WASM_ASYNC_CLIPBOARD 1022 // abort early if we already know it is not supported 1023 if (view->impl->supportsClipboardRead == PUGL_FALSE) { 1024 return PUGL_UNSUPPORTED; 1025 } 1026 1027 free(view->impl->clipboardData); 1028 view->impl->clipboardData = puglGetAsyncClipboardData(); 1029 #endif 1030 1031 if (view->impl->clipboardData == NULL) { 1032 return PUGL_FAILURE; 1033 } 1034 1035 const PuglDataOfferEvent offer = { 1036 PUGL_DATA_OFFER, 1037 0, 1038 emscripten_get_now() / 1e3, 1039 }; 1040 1041 PuglEvent offerEvent; 1042 offerEvent.offer = offer; 1043 puglDispatchEvent(view, &offerEvent); 1044 return PUGL_SUCCESS; 1045 } 1046 1047 PuglStatus 1048 puglAcceptOffer(PuglView* const view, 1049 const PuglDataOfferEvent* const offer, 1050 const uint32_t typeIndex) 1051 { 1052 if (typeIndex != 0) { 1053 return PUGL_UNSUPPORTED; 1054 } 1055 1056 const PuglDataEvent data = { 1057 PUGL_DATA, 1058 0, 1059 emscripten_get_now() / 1e3, 1060 0, 1061 }; 1062 1063 PuglEvent dataEvent; 1064 dataEvent.data = data; 1065 puglDispatchEvent(view, &dataEvent); 1066 return PUGL_SUCCESS; 1067 } 1068 1069 uint32_t 1070 puglGetNumClipboardTypes(const PuglView* const view) 1071 { 1072 return view->impl->clipboardData != NULL ? 1u : 0u; 1073 } 1074 1075 const char* 1076 puglGetClipboardType(const PuglView* const view, const uint32_t typeIndex) 1077 { 1078 return (typeIndex == 0 && view->impl->clipboardData != NULL) 1079 ? "text/plain" 1080 : NULL; 1081 } 1082 1083 const void* 1084 puglGetClipboard(PuglView* const view, 1085 const uint32_t typeIndex, 1086 size_t* const len) 1087 { 1088 return view->impl->clipboardData; 1089 } 1090 1091 PuglStatus 1092 puglSetClipboard(PuglView* const view, 1093 const char* const type, 1094 const void* const data, 1095 const size_t len) 1096 { 1097 // only utf8 text supported for now 1098 if (type != NULL && strcmp(type, "text/plain") != 0) { 1099 return PUGL_UNSUPPORTED; 1100 } 1101 1102 const char* const className = view->world->strings[PUGL_CLASS_NAME]; 1103 const char* const text = (const char*)data; 1104 1105 #ifdef PUGL_WASM_ASYNC_CLIPBOARD 1106 // abort early if we already know it is not supported 1107 if (view->impl->supportsClipboardWrite == PUGL_FALSE) { 1108 return PUGL_UNSUPPORTED; 1109 } 1110 #else 1111 puglSetString(&view->impl->clipboardData, text); 1112 #endif 1113 1114 EM_ASM({ 1115 if (typeof(navigator.clipboard) !== 'undefined' && typeof(navigator.clipboard.writeText) === 'function' && window.isSecureContext) { 1116 navigator.clipboard.writeText(UTF8ToString($1)); 1117 } else { 1118 var canvasClipboardObjName = UTF8ToString($0) + "_clipboard"; 1119 var canvasClipboardElem = document.getElementById(canvasClipboardObjName); 1120 1121 if (!canvasClipboardElem) { 1122 canvasClipboardElem = document.createElement('textarea'); 1123 canvasClipboardElem.id = canvasClipboardObjName; 1124 canvasClipboardElem.style.position = 'fixed'; 1125 canvasClipboardElem.style.whiteSpace = 'pre'; 1126 canvasClipboardElem.style.zIndex = '-1'; 1127 canvasClipboardElem.setAttribute('readonly', true); 1128 document.body.appendChild(canvasClipboardElem); 1129 } 1130 1131 canvasClipboardElem.textContent = UTF8ToString($1); 1132 canvasClipboardElem.select(); 1133 document.execCommand("copy"); 1134 } 1135 }, className, text); 1136 1137 // FIXME proper return status 1138 return PUGL_SUCCESS; 1139 } 1140 1141 PuglStatus 1142 puglSetCursor(PuglView* const view, const PuglCursor cursor) 1143 { 1144 printf("TODO: %s %d\n", __func__, __LINE__); 1145 return PUGL_FAILURE; 1146 } 1147 1148 PuglStatus 1149 puglSetTransientParent(PuglView* const view, const PuglNativeView parent) 1150 { 1151 printf("TODO: %s %d\n", __func__, __LINE__); 1152 view->transientParent = parent; 1153 return PUGL_FAILURE; 1154 } 1155 1156 PuglStatus 1157 puglSetPosition(PuglView* const view, const int x, const int y) 1158 { 1159 printf("TODO: %s %d\n", __func__, __LINE__); 1160 1161 if (x > INT16_MAX || y > INT16_MAX) { 1162 return PUGL_BAD_PARAMETER; 1163 } 1164 1165 if (!view->impl->created) { 1166 // Set defaults to be used when realized 1167 view->defaultX = x; 1168 view->defaultY = y; 1169 return PUGL_SUCCESS; 1170 } 1171 1172 view->lastConfigure.x = (PuglCoord)x; 1173 view->lastConfigure.y = (PuglCoord)y; 1174 return puglPostRedisplay(view); 1175 }