17#include "IGraphicsWeb.h"
19EM_JS(
int, iplug_wasm_capture_bridge_enabled, (), {
20 if (typeof window ===
"undefined")
return 0;
22 return new URLSearchParams(window.location.search).get(
"iplugWasmCapture") ===
"1";
29EM_JS(
int, createWebGLContextForShadowDOM, (), {
30 var canvas = Module.canvas;
31 if (!canvas)
return 0;
32 var preserveDrawingBuffer =
false;
34 preserveDrawingBuffer =
new URLSearchParams(window.location.search).get(
"iplugWasmCapture") ===
"1";
36 var attrs = { stencil:
true, depth:
true, antialias:
true, alpha:
true, preserveDrawingBuffer: preserveDrawingBuffer };
37 var ctx = canvas.getContext(
"webgl", attrs) || canvas.getContext(
"experimental-webgl", attrs);
39 return GL.registerContext(ctx, attrs);
42EM_JS(
void, iplug_popup_menu_show_js, (
void* pGraphics,
double viewportX,
double viewportY,
const char* menuJson), {
44 var emitSelection = function(pG, path) {
45 if (!pG || !Module._iplug_popup_menu_selected) {
50 Module.ccall(
'iplug_popup_menu_selected', null, [
'number',
'string'], [pG, path ||
""]);
52 Module._iplug_popup_menu_selected(pG, 0);
57 rootItems = JSON.parse(UTF8ToString(menuJson));
59 console.error(
'iPlug popup menu: invalid menu payload', e);
60 emitSelection(pGraphics,
"");
64 if (!HTMLElement.prototype.showPopover || !HTMLElement.prototype.hidePopover) {
65 emitSelection(pGraphics,
"");
70 if (!doc.getElementById(
'__iplug_popup_menu_style')) {
71 var style = doc.createElement(
'style');
72 style.id =
'__iplug_popup_menu_style';
74 '.iplug-popup-menu{box-sizing:border-box;position:fixed;inset:0;width:100vw;height:100vh;margin:0;padding:0;border:0;background:transparent;overflow:visible;pointer-events:none;color:var(--iplug-popup-menu-color,#ddd);font:var(--iplug-popup-menu-font,13px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif);}',
75 '.iplug-popup-menu::backdrop{background:transparent;}',
76 '.iplug-popup-menu__panel{box-sizing:border-box;position:absolute;padding:var(--iplug-popup-menu-padding,4px 0);min-width:var(--iplug-popup-menu-min-width,160px);max-height:var(--iplug-popup-menu-max-height,70vh);overflow-y:auto;border:var(--iplug-popup-menu-border,1px solid #555);border-radius:var(--iplug-popup-menu-border-radius,6px);background:var(--iplug-popup-menu-background,#1e1e1e);color:inherit;box-shadow:var(--iplug-popup-menu-shadow,0 6px 18px rgba(0,0,0,.4));pointer-events:auto;}',
77 '.iplug-popup-menu__item{display:flex;align-items:center;gap:.35em;width:100%;box-sizing:border-box;padding:var(--iplug-popup-menu-item-padding,5px 12px);border:0;background:transparent;color:inherit;text-align:left;font:inherit;white-space:nowrap;cursor:pointer;}',
78 '.iplug-popup-menu__item:disabled{cursor:default;opacity:var(--iplug-popup-menu-disabled-opacity,.45);}',
79 '.iplug-popup-menu__item:not(:disabled):hover,.iplug-popup-menu__item:not(:disabled):focus{background:var(--iplug-popup-menu-hover-background,#3b82f6);color:var(--iplug-popup-menu-hover-color,#fff);outline:0;}',
80 '.iplug-popup-menu__check{display:inline-block;width:1.25em;flex:0 0 1.25em;}',
81 '.iplug-popup-menu__submenu-indicator{margin-left:auto;padding-left:1.5em;}',
82 '.iplug-popup-menu__title{padding:var(--iplug-popup-menu-title-padding,6px 12px 2px);color:var(--iplug-popup-menu-title-color,#888);font-weight:600;font-size:var(--iplug-popup-menu-title-font-size,11px);text-transform:uppercase;}',
83 '.iplug-popup-menu__separator{height:1px;margin:var(--iplug-popup-menu-separator-margin,4px 8px);background:var(--iplug-popup-menu-separator-color,#444);}'
85 doc.head.appendChild(style);
88 var menu = doc.getElementById(
'__iplug_popup_menu');
90 menu = doc.createElement(
'div');
91 menu.id =
'__iplug_popup_menu';
92 menu.className =
'iplug-popup-menu';
93 menu.setAttribute(
'popover',
'manual');
94 menu.setAttribute(
'role',
'menu');
95 menu.setAttribute(
'tabindex',
'-1');
98 doc.body.appendChild(menu);
100 if (menu._iplugCleanup) {
101 menu._iplugCleanup(menu._iplugPGraphics && menu._iplugPGraphics !== pGraphics);
103 if (menu.matches && menu.matches(
':popover-open') && menu.hidePopover) {
106 while (menu.firstChild) {
107 menu.removeChild(menu.firstChild);
111 menu._iplugPickedPath =
"";
112 menu._iplugPGraphics = pGraphics;
114 var clearPanelsFrom = function(depth) {
115 Array.prototype.slice.call(menu.querySelectorAll(
'.iplug-popup-menu__panel')).forEach(function(panel) {
116 if (Number(panel.dataset.depth) >= depth) {
117 if (panel.dataset.path) {
118 var trigger = menu.querySelector(
'.iplug-popup-menu__item[data-iplug-path="' + panel.dataset.path +
'"]');
120 trigger.setAttribute(
'aria-expanded',
'false');
128 var getMenuItems = function(panel) {
132 return Array.prototype.slice.call(panel.querySelectorAll(
'.iplug-popup-menu__item:not(:disabled)'));
135 var focusButton = function(button) {
140 button.focus({ preventScroll:
true });
146 var focusMenuItem = function(panel, idx) {
147 var items = getMenuItems(panel);
151 var wrappedIdx = (idx + items.length) % items.length;
152 focusButton(items[wrappedIdx]);
155 var focusFirstMenuItem = function(panel) {
156 focusMenuItem(panel, 0);
159 var getActivePanel = function() {
160 var active = doc.activeElement;
161 if (active && active.classList && active.classList.contains(
'iplug-popup-menu__item')) {
162 return active.closest(
'.iplug-popup-menu__panel');
165 var panels = Array.prototype.slice.call(menu.querySelectorAll(
'.iplug-popup-menu__panel'));
166 return panels.length ? panels[panels.length - 1] : null;
169 var getActiveButton = function() {
170 var active = doc.activeElement;
171 if (active && active.classList && active.classList.contains(
'iplug-popup-menu__item')) {
177 var positionPanel = function(panel, x, y, fallbackRightEdge) {
179 panel.style.left =
'0px';
180 panel.style.top =
'0px';
181 panel.style.visibility =
'hidden';
183 var rect = panel.getBoundingClientRect();
187 if (left + rect.width + margin > window.innerWidth && fallbackRightEdge !== null) {
188 left = fallbackRightEdge - rect.width;
191 left = Math.max(margin, Math.min(left, window.innerWidth - rect.width - margin));
192 top = Math.max(margin, Math.min(top, window.innerHeight - rect.height - margin));
194 panel.style.left = Math.round(left) +
'px';
195 panel.style.top = Math.round(top) +
'px';
196 panel.style.visibility =
"";
200 var appendMenuItem = function(panel, item, idx, path, depth) {
201 var childPath = path.concat([idx]);
203 if (item.separator) {
204 var separator = doc.createElement(
'div');
205 separator.className =
'iplug-popup-menu__separator';
206 separator.setAttribute(
'role',
'separator');
207 separator.addEventListener(
'pointerenter', function() {
208 clearPanelsFrom(depth + 1);
210 panel.appendChild(separator);
215 var title = doc.createElement(
'div');
216 title.className =
'iplug-popup-menu__title';
217 title.setAttribute(
'role',
'presentation');
218 title.textContent = item.text ||
"";
219 title.addEventListener(
'pointerenter', function() {
220 clearPanelsFrom(depth + 1);
222 panel.appendChild(title);
226 var button = doc.createElement(
'button');
227 var hasSubmenu = Array.isArray(item.submenu) && item.submenu.length > 0;
228 button.className =
'iplug-popup-menu__item';
229 button.type =
'button';
230 button.setAttribute(
'role',
'menuitem');
231 button.dataset.iplugPath = childPath.join(
',');
232 button.disabled = !!item.disabled || (!hasSubmenu && !!item.submenu);
234 button.setAttribute(
'role',
'menuitemcheckbox');
235 button.setAttribute(
'aria-checked',
'true');
238 var check = doc.createElement(
'span');
239 check.className =
'iplug-popup-menu__check';
240 check.textContent = item.checked ? String.fromCharCode(0x2713) :
"";
241 button.appendChild(check);
243 var label = doc.createElement(
'span');
244 label.textContent = item.text ||
"";
245 button.appendChild(label);
248 button.setAttribute(
'aria-haspopup',
'menu');
249 button.setAttribute(
'aria-expanded',
'false');
251 var submenuIndicator = doc.createElement(
'span');
252 submenuIndicator.className =
'iplug-popup-menu__submenu-indicator';
253 submenuIndicator.textContent = String.fromCharCode(0x203a);
254 button.appendChild(submenuIndicator);
256 var openSubmenu = function(focusFirst) {
257 if (button.disabled) {
261 var rect = button.getBoundingClientRect();
262 var childPanel = showPanel(item.submenu, childPath, depth + 1, rect.right - 1, rect.top, rect.left + 1);
263 button.setAttribute(
'aria-expanded',
'true');
265 focusFirstMenuItem(childPanel);
268 button._iplugOpenSubmenu = openSubmenu;
270 button.addEventListener(
'pointerenter', function() {
273 button.addEventListener(
'click', function(e) {
278 button.addEventListener(
'pointerenter', function() {
279 clearPanelsFrom(depth + 1);
281 button.addEventListener(
'click', function() {
282 if (button.disabled) {
285 menu._iplugPickedPath = childPath.join(
',');
290 panel.appendChild(button);
293 showPanel = function(items, path, depth, x, y, fallbackRightEdge) {
294 clearPanelsFrom(depth);
296 var panel = doc.createElement(
'div');
297 panel.className =
'iplug-popup-menu__panel';
298 panel.dataset.depth = String(depth);
299 panel.dataset.path = path.join(
',');
300 panel.setAttribute(
'role',
'menu');
302 items.forEach(function(item, idx) {
303 appendMenuItem(panel, item, idx, path, depth);
306 menu.appendChild(panel);
307 positionPanel(panel, x, y, fallbackRightEdge);
311 var onDocPointerDown = function(e) {
312 if (menu.contains(e.target)) {
318 var moveFocus = function(delta) {
319 var panel = getActivePanel();
320 var items = getMenuItems(panel);
325 var idx = items.indexOf(getActiveButton());
327 focusButton(items[delta > 0 ? 0 : items.length - 1]);
329 focusButton(items[(idx + delta + items.length) % items.length]);
333 var closeActiveSubmenu = function() {
334 var panel = getActivePanel();
339 var depth = Number(panel.dataset.depth);
344 var path = panel.dataset.path ||
"";
345 clearPanelsFrom(depth);
346 focusButton(menu.querySelector(
'.iplug-popup-menu__item[data-iplug-path="' + path +
'"]'));
350 var onKeyDown = function(e) {
352 var activeButton = getActiveButton();
365 focusMenuItem(getActivePanel(), 0);
368 var panel = getActivePanel();
369 focusMenuItem(panel, getMenuItems(panel).length - 1);
372 if (activeButton && activeButton._iplugOpenSubmenu) {
373 activeButton._iplugOpenSubmenu(
true);
377 closeActiveSubmenu();
383 if (activeButton._iplugOpenSubmenu) {
384 activeButton._iplugOpenSubmenu(
true);
386 activeButton.click();
401 var finish = function(sendCallback) {
402 menu.removeEventListener(
'toggle', onToggle);
403 doc.removeEventListener(
'pointerdown', onDocPointerDown,
true);
404 doc.removeEventListener(
'keydown', onKeyDown,
true);
405 menu._iplugCleanup = null;
407 var picked = menu._iplugPickedPath;
408 var pG = menu._iplugPGraphics;
409 menu._iplugPickedPath =
"";
410 menu._iplugPGraphics = null;
413 emitSelection(pG, picked);
417 var onToggle = function(e) {
418 if (e.newState ===
'closed') {
423 menu._iplugCleanup = finish;
424 menu.addEventListener(
'toggle', onToggle);
426 setTimeout(function() {
427 if (menu._iplugCleanup !== finish || menu._iplugPGraphics !== pGraphics) {
434 console.warn(
'iPlug popup menu: showPopover failed', e);
439 showPanel(rootItems, [], 0, viewportX, viewportY, null);
442 menu.focus({ preventScroll:
true });
447 setTimeout(function() {
448 if (menu._iplugCleanup === finish && menu._iplugPGraphics === pGraphics) {
449 doc.addEventListener(
'pointerdown', onDocPointerDown, true);
450 doc.addEventListener(
'keydown', onKeyDown, true);
456EM_JS(
void, iplug_popup_menu_close_js, (
void* pGraphics), {
457 var menu = document.getElementById(
'__iplug_popup_menu');
458 if (!menu || menu._iplugPGraphics !== pGraphics) {
462 menu._iplugPGraphics = null;
463 menu._iplugPickedPath =
"";
464 if (menu.matches && menu.matches(
':popover-open') && menu.hidePopover) {
466 }
else if (menu._iplugCleanup) {
467 menu._iplugCleanup(false);
471#if !defined(NDEBUG) || defined(IPLUG_LIVE_EDIT)
472EM_JS(
void, iplug_emit_live_edit_js, (
const char* eventJson), {
475 msg = JSON.parse(UTF8ToString(eventJson));
477 console.error(
'iPlug live edit: invalid event payload', e);
485 window.postMessage(msg,
'*');
486 if (window.parent && window.parent !== window) {
487 window.parent.postMessage(msg,
'*');
490 console.warn(
'iPlug live edit: postMessage failed', e);
497BEGIN_IGRAPHICS_NAMESPACE
499void GetScreenDimensions(
int& width,
int& height)
501 width = val::global(
"window")[
"innerWidth"].as<
int>();
502 height = val::global(
"window")[
"innerHeight"].as<
int>();
506END_IGRAPHICS_NAMESPACE
508using namespace iplug;
509using namespace igraphics;
510using namespace emscripten;
512extern std::vector<IGraphicsWeb*> gGraphicsInstances;
513extern void UnregisterGraphicsInstance(
IGraphicsWeb* pGraphics);
514double gPrevMouseDownTime = 0.;
515bool gFirstClick =
false;
517#pragma mark - Private Classes and Structs
521class IGraphicsWeb::Font :
public PlatformFont
524 Font(
const char* fontName,
const char* fontStyle)
525 : PlatformFont(true), mDescriptor{fontName, fontStyle}
528 FontDescriptor GetDescriptor()
override {
return &mDescriptor; }
531 std::pair<WDL_String, WDL_String> mDescriptor;
537 FileFont(
const char* fontName,
const char* fontStyle,
const char* fontPath)
538 : Font(fontName, fontStyle), mPath(fontPath)
543 IFontDataPtr GetFontData()
override;
549IFontDataPtr IGraphicsWeb::FileFont::GetFontData()
551 IFontDataPtr fontData(
new IFontData());
552 FILE* fp = fopen(mPath.Get(),
"rb");
558 fseek(fp,0,SEEK_END);
559 fontData = std::make_unique<IFontData>((
int) ftell(fp));
561 if (!fontData->GetSize())
564 fseek(fp,0,SEEK_SET);
565 size_t readSize = fread(fontData->Get(), 1, fontData->GetSize(), fp);
568 if (readSize && readSize == fontData->GetSize())
569 fontData->SetFaceIdx(0);
577 MemoryFont(
const char* fontName,
const char* fontStyle,
const void* pData,
int dataSize)
578 : Font(fontName, fontStyle)
581 mData.Set((
const uint8_t*)pData, dataSize);
584 IFontDataPtr GetFontData()
override
586 return IFontDataPtr(
new IFontData(mData.Get(), mData.GetSize(), 0));
590 WDL_TypedBuf<uint8_t> mData;
593#pragma mark - Utilities and Callbacks
595static EM_BOOL key_callback(
int eventType,
const EmscriptenKeyboardEvent* pEvent,
void* pUserData)
603 if ((VK >= kVK_0 && VK <= kVK_Z) || VK == kVK_NONE)
604 keyUTF8.Set(pEvent->key);
610 static_cast<bool>(pEvent->shiftKey),
611 static_cast<bool>(pEvent->ctrlKey || pEvent->metaKey),
612 static_cast<bool>(pEvent->altKey)};
616 case EMSCRIPTEN_EVENT_KEYDOWN:
618 return pGraphicsWeb->OnKeyDown(pGraphicsWeb->mPrevX, pGraphicsWeb->mPrevY, keyPress);
620 case EMSCRIPTEN_EVENT_KEYUP:
622 return pGraphicsWeb->OnKeyUp(pGraphicsWeb->mPrevX, pGraphicsWeb->mPrevY, keyPress);
631static EM_BOOL outside_mouse_callback(
int eventType,
const EmscriptenMouseEvent* pEvent,
void* pUserData)
636 val rect = pGraphics->
GetCanvas().call<val>(
"getBoundingClientRect");
637 info.x = (pEvent->targetX - rect[
"left"].as<
double>()) / pGraphics->GetDrawScale();
638 info.y = (pEvent->targetY - rect[
"top"].as<
double>()) / pGraphics->GetDrawScale();
639 info.dX = pEvent->movementX;
640 info.dY = pEvent->movementY;
641 info.ms = {(pEvent->buttons & 1) != 0, (pEvent->buttons & 2) != 0,
static_cast<bool>(pEvent->shiftKey),
static_cast<bool>(pEvent->ctrlKey),
static_cast<bool>(pEvent->altKey)};
642 std::vector<IMouseInfo> list {info};
646 case EMSCRIPTEN_EVENT_MOUSEUP:
649 list[0].ms.L = pEvent->button == 0;
650 list[0].ms.R = pEvent->button == 2;
651 pGraphics->OnMouseUp(list);
652 emscripten_set_mousemove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, pGraphics, 1,
nullptr);
653 emscripten_set_mouseup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, pGraphics, 1,
nullptr);
656 case EMSCRIPTEN_EVENT_MOUSEMOVE:
658 if(pEvent->buttons != 0 && !pGraphics->IsInPlatformTextEntry())
659 pGraphics->OnMouseDrag(list);
666 pGraphics->mPrevX = info.x;
667 pGraphics->mPrevY = info.y;
672static EM_BOOL mouse_callback(
int eventType,
const EmscriptenMouseEvent* pEvent,
void* pUserData)
677 info.x = pEvent->targetX / pGraphics->GetDrawScale();
678 info.y = pEvent->targetY / pGraphics->GetDrawScale();
679 info.dX = pEvent->movementX;
680 info.dY = pEvent->movementY;
681 info.ms = {(pEvent->buttons & 1) != 0,
682 (pEvent->buttons & 2) != 0,
683 static_cast<bool>(pEvent->shiftKey),
684 static_cast<bool>(pEvent->ctrlKey),
685 static_cast<bool>(pEvent->altKey)};
687 std::vector<IMouseInfo> list {info};
690 case EMSCRIPTEN_EVENT_MOUSEDOWN:
692 const double timestamp = GetTimestamp();
693 const double timeDiff = timestamp - gPrevMouseDownTime;
695 if (gFirstClick && timeDiff < 0.3)
698 pGraphics->OnMouseDblClick(info.x, info.y, info.ms);
703 pGraphics->OnMouseDown(list);
706 gPrevMouseDownTime = timestamp;
710 case EMSCRIPTEN_EVENT_MOUSEUP:
713 list[0].ms.L = pEvent->button == 0;
714 list[0].ms.R = pEvent->button == 2;
715 pGraphics->OnMouseUp(list);
718 case EMSCRIPTEN_EVENT_MOUSEMOVE:
722 if(pEvent->buttons == 0)
723 pGraphics->OnMouseOver(info.x, info.y, info.ms);
726 if(!pGraphics->IsInPlatformTextEntry())
727 pGraphics->OnMouseDrag(list);
731 case EMSCRIPTEN_EVENT_MOUSEENTER:
732 pGraphics->OnSetCursor();
733 pGraphics->OnMouseOver(info.x, info.y, info.ms);
734 emscripten_set_mousemove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, pGraphics, 1,
nullptr);
736 case EMSCRIPTEN_EVENT_MOUSELEAVE:
737 if(pEvent->buttons != 0)
739 emscripten_set_mousemove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, pGraphics, 1, outside_mouse_callback);
740 emscripten_set_mouseup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, pGraphics, 1, outside_mouse_callback);
742 pGraphics->OnMouseOut();
break;
747 pGraphics->mPrevX = info.x;
748 pGraphics->mPrevY = info.y;
753static EM_BOOL wheel_callback(
int eventType,
const EmscriptenWheelEvent* pEvent,
void* pUserData)
757 IMouseMod modifiers(
false,
false, pEvent->mouse.shiftKey, pEvent->mouse.ctrlKey, pEvent->mouse.altKey);
759 double x = pEvent->mouse.targetX;
760 double y = pEvent->mouse.targetY;
766 case EMSCRIPTEN_EVENT_WHEEL: pGraphics->
OnMouseWheel(x, y, modifiers, pEvent->deltaY);
774EM_BOOL touch_callback(
int eventType,
const EmscriptenTouchEvent* pEvent,
void* pUserData)
779 std::vector<IMouseInfo> points;
781 static EmscriptenTouchPoint previousTouches[32];
783 for (
auto i = 0; i < pEvent->numTouches; i++)
786 info.x = pEvent->touches[i].targetX / drawScale;
787 info.y = pEvent->touches[i].targetY / drawScale;
788 info.dX = info.x - (previousTouches[i].targetX / drawScale);
789 info.dY = info.y - (previousTouches[i].targetY / drawScale);
792 static_cast<bool>(pEvent->shiftKey),
793 static_cast<bool>(pEvent->ctrlKey),
794 static_cast<bool>(pEvent->altKey),
795 static_cast<ITouchID
>(pEvent->touches[i].identifier)
798 if(pEvent->touches[i].isChanged)
799 points.push_back(info);
802 memcpy(previousTouches, pEvent->touches,
sizeof(previousTouches));
806 case EMSCRIPTEN_EVENT_TOUCHSTART:
809 case EMSCRIPTEN_EVENT_TOUCHEND:
812 case EMSCRIPTEN_EVENT_TOUCHMOVE:
815 case EMSCRIPTEN_EVENT_TOUCHCANCEL:
823static EM_BOOL complete_text_entry(
int eventType,
const EmscriptenFocusEvent* focusEvent,
void* pUserData)
827 val input = val::global(
"document").call<val>(
"getElementById", std::string(
"textEntry"));
828 std::string str = input[
"value"].as<std::string>();
829 val::global(
"document")[
"body"].call<
void>(
"removeChild", input);
830 pGraphics->SetControlValueAfterTextEdit(str.c_str());
835static EM_BOOL text_entry_keydown(
int eventType,
const EmscriptenKeyboardEvent* pEvent,
void* pUserData)
840 static_cast<bool>(pEvent->shiftKey),
841 static_cast<bool>(pEvent->ctrlKey),
842 static_cast<bool>(pEvent->altKey)};
844 if (keyPress.VK == kVK_RETURN || keyPress.VK == kVK_TAB)
845 return complete_text_entry(0,
nullptr, pUserData);
850static EM_BOOL uievent_callback(
int eventType,
const EmscriptenUiEvent* pEvent,
void* pUserData)
854 if (eventType == EMSCRIPTEN_EVENT_RESIZE)
856 pGraphics->GetDelegate()->OnParentWindowResize(pEvent->windowInnerWidth, pEvent->windowInnerHeight);
864IColorPickerHandlerFunc gColorPickerHandlerFunc =
nullptr;
866static void color_picker_callback(val e)
868 if(gColorPickerHandlerFunc)
870 std::string colorStrHex = e[
"target"][
"value"].as<std::string>();
872 if (colorStrHex[0] ==
'#')
873 colorStrHex = colorStrHex.erase(0, 1);
877 sscanf(colorStrHex.c_str(),
"%02x%02x%02x", &result.R, &result.G, &result.B);
879 gColorPickerHandlerFunc(result);
883static void file_dialog_callback(val e)
888EMSCRIPTEN_BINDINGS(events) {
889 function(
"color_picker_callback", color_picker_callback);
890 function(
"file_dialog_callback", file_dialog_callback);
896: IGRAPHICS_DRAW_CLASS(dlg, w, h, fps, scale)
898#if !defined(NDEBUG) || defined(IPLUG_LIVE_EDIT)
899 SetLiveEditEventFunc([](
const char* eventJson) {
900 iplug_emit_live_edit_js(eventJson);
904 val keys = val::global(
"Object").call<val>(
"keys", GetPreloadedImages());
906 DBGMSG(
"Preloaded %i images\n", keys[
"length"].as<int>());
909 if (canvas.isUndefined() || canvas.isNull())
912 val moduleCanvas = val::global(
"Module")[
"canvas"];
913 if (!moduleCanvas.isUndefined() && !moduleCanvas.isNull())
915 mCanvas = moduleCanvas;
920 mCanvas = val::global(
"document").call<val>(
"getElementById", std::string(
"canvas"));
929 mRootNode = mCanvas.call<val>(
"getRootNode");
930 std::string rootNodeType = mRootNode[
"constructor"][
"name"].as<std::string>();
931 mInShadowDOM = (rootNodeType ==
"ShadowRoot");
936 std::string existingId = mCanvas[
"id"].as<std::string>();
937 if (existingId.empty())
940 snprintf(idBuf,
sizeof(idBuf),
"iplug-canvas-%p",
static_cast<void*
>(
this));
942 mCanvas.set(
"id", existingId);
944 mCanvasSelector = std::string(
"#") + existingId;
946 DBGMSG(
"IGraphicsWeb: Shadow DOM = %s, selector = %s\n", mInShadowDOM ?
"true" :
"false", mCanvasSelector.c_str());
948 RegisterCanvasEvents();
951void IGraphicsWeb::RegisterCanvasEvents()
954 emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW,
this, 1, key_callback);
955 emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW,
this, 1, key_callback);
956 emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW,
this, 1, uievent_callback);
964 var canvas = Module.canvas;
968 canvas._iplugGraphics = pGraphics;
970 canvas.addEventListener(
'mousedown', function(e) {
971 Module._iGraphicsMouseCallback(pGraphics, 0, e.offsetX, e.offsetY, e.movementX, e.movementY, e.buttons, e.button, e.shiftKey, e.ctrlKey, e.altKey);
973 canvas.addEventListener(
'mouseup', function(e) {
974 Module._iGraphicsMouseCallback(pGraphics, 1, e.offsetX, e.offsetY, e.movementX, e.movementY, e.buttons, e.button, e.shiftKey, e.ctrlKey, e.altKey);
976 canvas.addEventListener(
'mousemove', function(e) {
977 Module._iGraphicsMouseCallback(pGraphics, 2, e.offsetX, e.offsetY, e.movementX, e.movementY, e.buttons, e.button, e.shiftKey, e.ctrlKey, e.altKey);
979 canvas.addEventListener(
'mouseenter', function(e) {
980 Module._iGraphicsMouseCallback(pGraphics, 3, e.offsetX, e.offsetY, e.movementX, e.movementY, e.buttons, e.button, e.shiftKey, e.ctrlKey, e.altKey);
982 canvas.addEventListener(
'mouseleave', function(e) {
983 Module._iGraphicsMouseCallback(pGraphics, 4, e.offsetX, e.offsetY, e.movementX, e.movementY, e.buttons, e.button, e.shiftKey, e.ctrlKey, e.altKey);
985 canvas.addEventListener(
'wheel', function(e) {
986 Module._iGraphicsWheelCallback(pGraphics, e.offsetX, e.offsetY, e.deltaY, e.shiftKey, e.ctrlKey, e.altKey);
988 }, { passive:
false });
995 const char* target = mCanvasSelector.c_str();
996 emscripten_set_mousedown_callback(target,
this, 1, mouse_callback);
997 emscripten_set_mouseup_callback(target,
this, 1, mouse_callback);
998 emscripten_set_mousemove_callback(target,
this, 1, mouse_callback);
999 emscripten_set_mouseenter_callback(target,
this, 1, mouse_callback);
1000 emscripten_set_mouseleave_callback(target,
this, 1, mouse_callback);
1001 emscripten_set_wheel_callback(target,
this, 1, wheel_callback);
1002 emscripten_set_touchstart_callback(target,
this, 1, touch_callback);
1003 emscripten_set_touchend_callback(target,
this, 1, touch_callback);
1004 emscripten_set_touchmove_callback(target,
this, 1, touch_callback);
1005 emscripten_set_touchcancel_callback(target,
this, 1, touch_callback);
1009void IGraphicsWeb::UnregisterCanvasEvents()
1011 const char* target = mCanvasSelector.c_str();
1013 emscripten_set_mousedown_callback(target,
this, 1,
nullptr);
1014 emscripten_set_mouseup_callback(target,
this, 1,
nullptr);
1015 emscripten_set_mousemove_callback(target,
this, 1,
nullptr);
1016 emscripten_set_mouseenter_callback(target,
this, 1,
nullptr);
1017 emscripten_set_mouseleave_callback(target,
this, 1,
nullptr);
1018 emscripten_set_wheel_callback(target,
this, 1,
nullptr);
1019 emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW,
this, 1,
nullptr);
1020 emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW,
this, 1,
nullptr);
1021 emscripten_set_touchstart_callback(target,
this, 1,
nullptr);
1022 emscripten_set_touchend_callback(target,
this, 1,
nullptr);
1023 emscripten_set_touchmove_callback(target,
this, 1,
nullptr);
1024 emscripten_set_touchcancel_callback(target,
this, 1,
nullptr);
1027IGraphicsWeb::~IGraphicsWeb()
1029 iplug_popup_menu_close_js(
this);
1030 UnregisterCanvasEvents();
1031 UnregisterGraphicsInstance(
this);
1034void* IGraphicsWeb::OpenWindow(
void* pHandle)
1037 EmscriptenWebGLContextAttributes attr;
1038 emscripten_webgl_init_context_attributes(&attr);
1039 attr.stencil =
true;
1041 attr.antialias =
true;
1045 attr.preserveDrawingBuffer = iplug_wasm_capture_bridge_enabled();
1048 EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx;
1053 ctx = createWebGLContextForShadowDOM();
1058 ctx = emscripten_webgl_create_context(mCanvasSelector.c_str(), &attr);
1061 emscripten_webgl_make_context_current(ctx);
1064 OnViewInitialized(
nullptr );
1066 SetScreenScale(std::ceil(std::max(emscripten_get_device_pixel_ratio(), 1.)));
1068 GetDelegate()->LayoutUI(
this);
1069 GetDelegate()->OnUIOpen();
1074void IGraphicsWeb::HideMouseCursor(
bool hide,
bool lock)
1076 if (mCursorHidden == hide)
1081#ifdef IGRAPHICS_WEB_POINTERLOCK
1083 emscripten_request_pointerlock(mCanvasSelector.c_str(), EM_FALSE);
1086 mCanvas[
"style"].set(
"cursor",
"none");
1088 mCursorHidden =
true;
1093#ifdef IGRAPHICS_WEB_POINTERLOCK
1095 emscripten_exit_pointerlock();
1100 mCursorHidden =
false;
1101 mCursorLock =
false;
1105ECursor IGraphicsWeb::SetMouseCursor(ECursor cursorType)
1107 std::string cursor(
"pointer");
1111 case ECursor::ARROW: cursor =
"default";
break;
1112 case ECursor::IBEAM: cursor =
"text";
break;
1113 case ECursor::WAIT: cursor =
"wait";
break;
1114 case ECursor::CROSS: cursor =
"crosshair";
break;
1115 case ECursor::UPARROW: cursor =
"n-resize";
break;
1116 case ECursor::SIZENWSE: cursor =
"nwse-resize";
break;
1117 case ECursor::SIZENESW: cursor =
"nesw-resize";
break;
1118 case ECursor::SIZEWE: cursor =
"ew-resize";
break;
1119 case ECursor::SIZENS: cursor =
"ns-resize";
break;
1120 case ECursor::SIZEALL: cursor =
"move";
break;
1121 case ECursor::INO: cursor =
"not-allowed";
break;
1122 case ECursor::HAND: cursor =
"pointer";
break;
1123 case ECursor::APPSTARTING: cursor =
"progress";
break;
1124 case ECursor::HELP: cursor =
"help";
break;
1127 mCanvas[
"style"].set(
"cursor", cursor);
1131void IGraphicsWeb::GetMouseLocation(
float& x,
float&y)
const
1138void IGraphicsWeb::OnMainLoopTimer()
1140 int screenScale = (int) std::ceil(std::max(emscripten_get_device_pixel_ratio(), 1.));
1145 if (pGraphics ==
nullptr)
1148 if (screenScale != pGraphics->GetScreenScale())
1150 pGraphics->SetScreenScale(screenScale);
1154 if (pGraphics->IsDirty(rects))
1156 pGraphics->SetAllControlsClean();
1157 pGraphics->Draw(rects);
1162EMsgBoxResult IGraphicsWeb::ShowMessageBox(
const char* str,
const char* , EMsgBoxType type, IMsgBoxCompletionHandlerFunc completionHandler)
1164 ReleaseMouseCapture();
1166 EMsgBoxResult result = kNoResult;
1172 val::global(
"window").call<val>(
"alert", std::string(str));
1173 result = EMsgBoxResult::kOK;
1179 result =
static_cast<EMsgBoxResult
>(val::global(
"window").call<val>(
"confirm", std::string(str)).as<int>());
1184 return result = kNoResult;
1187 if(completionHandler)
1188 completionHandler(result);
1193void IGraphicsWeb::PromptForFile(WDL_String& filename, WDL_String& path, EFileAction action,
const char* ext, IFileDialogCompletionHandlerFunc completionHandler)
1205void IGraphicsWeb::PromptForDirectory(WDL_String& path, IFileDialogCompletionHandlerFunc completionHandler)
1218bool IGraphicsWeb::PromptForColor(
IColor& color,
const char* str, IColorPickerHandlerFunc func)
1220 ReleaseMouseCapture();
1222 gColorPickerHandlerFunc = func;
1224 val inputEl = val::global(
"document").call<val>(
"createElement", std::string(
"input"));
1225 inputEl.call<
void>(
"setAttribute", std::string(
"type"), std::string(
"color"));
1226 WDL_String colorStr;
1227 colorStr.SetFormatted(64,
"#%02x%02x%02x", color.R, color.G, color.B);
1228 inputEl.call<
void>(
"setAttribute", std::string(
"value"), std::string(colorStr.Get()));
1229 inputEl.call<
void>(
"click");
1230 inputEl.call<
void>(
"addEventListener", std::string(
"input"), val::module_property(
"color_picker_callback"),
false);
1231 inputEl.call<
void>(
"addEventListener", std::string(
"onChange"), val::module_property(
"color_picker_callback"),
false);
1236void IGraphicsWeb::CreatePlatformTextEntry(
int paramIdx,
const IText& text,
const IRECT& bounds,
int length,
const char* str)
1238 val input = val::global(
"document").call<val>(
"createElement", std::string(
"input"));
1239 const val rect = mCanvas.call<val>(
"getBoundingClientRect");
1241 auto setDim = [&input](
const char *dimName,
double pixels)
1244 dimstr.SetFormatted(32,
"%fpx", pixels);
1245 input[
"style"].set(dimName, std::string(dimstr.Get()));
1248 auto setColor = [&input](
const char *colorName,
IColor color)
1251 str.SetFormatted(64,
"rgba(%d, %d, %d, %d)", color.R, color.G, color.B, color.A);
1252 input[
"style"].set(colorName, std::string(str.Get()));
1255 input.set(
"id", std::string(
"textEntry"));
1256 input[
"style"].set(
"position", val(
"fixed"));
1257 setDim(
"left", rect[
"left"].as<double>() + bounds.L);
1258 setDim(
"top", rect[
"top"].as<double>() + bounds.T);
1259 setDim(
"width", bounds.
W());
1260 setDim(
"height", bounds.
H());
1262 setColor(
"color", text.mTextEntryFGColor);
1263 setColor(
"background-color", text.mTextEntryBGColor);
1264 if (paramIdx > kNoParameter)
1266 const IParam* pParam = GetDelegate()->GetParam(paramIdx);
1268 switch (pParam->
Type())
1270 case IParam::kTypeEnum:
1271 case IParam::kTypeInt:
1272 case IParam::kTypeBool:
1273 input.set(
"type", val(
"number"));
1275 case IParam::kTypeDouble:
1276 input.set(
"type", val(
"number"));
1284 input.set(
"type", val(
"text"));
1290 mRootNode.call<
void>(
"appendChild", input);
1294 val::global(
"document")[
"body"].call<
void>(
"appendChild", input);
1297 input.call<
void>(
"focus");
1298 emscripten_set_focusout_callback(
"textEntry",
this, 1, complete_text_entry);
1299 emscripten_set_keydown_callback(
"textEntry",
this, 1, text_entry_keydown);
1304 void AppendJsonString(std::string& out,
const char* str)
1314 for (
const char* p = str; *p; ++p)
1316 const unsigned char c =
static_cast<unsigned char>(*p);
1320 case '\\': out +=
"\\\\";
break;
1321 case '"': out +=
"\\\"";
break;
1322 case '\b': out +=
"\\b";
break;
1323 case '\f': out +=
"\\f";
break;
1324 case '\n': out +=
"\\n";
break;
1325 case '\r': out +=
"\\r";
break;
1326 case '\t': out +=
"\\t";
break;
1331 std::snprintf(esc,
sizeof(esc),
"\\u%04x", c);
1336 out.push_back(
static_cast<char>(c));
1347 const char* itemText = item.GetText();
1349 if (!menu.GetPrefix() || item.GetIsSeparator())
1350 return itemText ? itemText :
"";
1353 switch (menu.GetPrefix())
1355 case 1: std::snprintf(prefix,
sizeof(prefix),
"%1d: ", itemIdx + 1);
break;
1356 case 2: std::snprintf(prefix,
sizeof(prefix),
"%02d: ", itemIdx + 1);
break;
1357 case 3: std::snprintf(prefix,
sizeof(prefix),
"%03d: ", itemIdx + 1);
break;
1358 default: prefix[0] =
'\0';
break;
1361 return std::string(prefix) + (itemText ? itemText :
"");
1364 void AppendPopupMenuJson(std::string& json,
IPopupMenu& menu)
1366 json.push_back(
'[');
1368 for (
int i = 0; i < menu.NItems(); ++i)
1372 json.push_back(
',');
1374 json.push_back(
'{');
1375 json +=
"\"text\":";
1376 std::string itemText;
1379 itemText = GetPopupMenuItemText(menu, i, *pItem);
1381 AppendJsonString(json, itemText.c_str());
1385 if (pItem->GetIsSeparator()) json +=
",\"separator\":true";
1386 if (pItem->GetIsTitle()) json +=
",\"title\":true";
1387 if (pItem->GetChecked()) json +=
",\"checked\":true";
1388 if (!pItem->GetEnabled()) json +=
",\"disabled\":true";
1390 if (
IPopupMenu* pSubmenu = pItem->GetSubmenu())
1392 json +=
",\"submenu\":";
1393 AppendPopupMenuJson(json, *pSubmenu);
1397 json.push_back(
'}');
1400 json.push_back(
']');
1403 bool ReadPopupMenuPathIndex(
const char*& path,
int& idx)
1405 if (!path || *path < '0' || *path >
'9')
1409 while (*path >=
'0' && *path <=
'9')
1411 idx = (idx * 10) + (*path -
'0');
1418 bool ResolvePopupMenuPath(
IPopupMenu& rootMenu,
const char* path,
IPopupMenu*& pSelectedMenu,
int& selectedIdx)
1420 if (!path || !*path)
1424 const char* p = path;
1429 if (!ReadPopupMenuPathIndex(p, idx))
1439 pMenu = pItem->GetSubmenu();
1449 pSelectedMenu = pMenu;
1461 mCurrentPopupMenu = &menu;
1462 menu.SetChosenItemIdx(-1);
1465 json.reserve(64 + (menu.NItems() * 48));
1466 AppendPopupMenuJson(json, menu);
1468 const val rect = mCanvas.call<val>(
"getBoundingClientRect");
1469 const double scale =
static_cast<double>(GetDrawScale());
1470 const double viewportX = rect[
"left"].as<
double>() + (bounds.L * scale);
1471 const double viewportY = rect[
"top"].as<
double>() + (bounds.B * scale);
1472 iplug_popup_menu_show_js(
this, viewportX, viewportY, json.c_str());
1477void IGraphicsWeb::OnPopupMenuSelectedAsync(
const char* path)
1480 mCurrentPopupMenu =
nullptr;
1487 if (!ResolvePopupMenuPath(*pRootMenu, path, pMenu, idx))
1489 SetControlValueAfterPopupMenu(
nullptr);
1494 if (pItem && pItem->GetIsChoosable())
1496 pMenu->SetChosenItemIdx(idx);
1498 if (pMenu->GetFunction())
1499 pMenu->ExecFunction();
1501 SetControlValueAfterPopupMenu(pMenu);
1505 SetControlValueAfterPopupMenu(
nullptr);
1509extern "C" EMSCRIPTEN_KEEPALIVE
1510void iplug_popup_menu_selected(
void* pGraphics,
const char* path)
1513 static_cast<IGraphicsWeb*
>(pGraphics)->OnPopupMenuSelectedAsync(path);
1516bool IGraphicsWeb::OpenURL(
const char* url,
const char* msgWindowTitle,
const char* confirmMsg,
const char* errMsgOnFailure)
1518 val::global(
"window").call<val>(
"open", std::string(url), std::string(
"_blank"));
1523void IGraphicsWeb::DrawResize()
1526 std::string widthPx = std::to_string(
static_cast<int>(Width() * GetDrawScale())) +
"px";
1527 std::string heightPx = std::to_string(
static_cast<int>(Height() * GetDrawScale())) +
"px";
1528 mCanvas[
"style"].set(
"width", val(widthPx));
1529 mCanvas[
"style"].set(
"height", val(heightPx));
1535 const int newBufW = Width() * GetBackingPixelScale();
1536 const int newBufH = Height() * GetBackingPixelScale();
1537 const int curBufW = mCanvas[
"width"].as<
int>();
1538 const int curBufH = mCanvas[
"height"].as<
int>();
1539 if (newBufW != curBufW || newBufH != curBufH)
1541 mCanvas.set(
"width", newBufW);
1542 mCanvas.set(
"height", newBufH);
1545 IGRAPHICS_DRAW_CLASS::DrawResize();
1548void IGraphicsWeb::PostResize()
1559 rects.
Add(GetBounds());
1560 SetAllControlsClean();
1564PlatformFontPtr IGraphicsWeb::LoadPlatformFont(
const char* fontID,
const char* fileNameOrResID)
1566 WDL_String fullPath;
1567 const EResourceLocation fontLocation =
LocateResource(fileNameOrResID,
"ttf", fullPath, GetBundleID(),
nullptr,
nullptr);
1569 if (fontLocation == kNotFound)
1572 return PlatformFontPtr(
new FileFont(fontID,
"", fullPath.Get()));
1575PlatformFontPtr IGraphicsWeb::LoadPlatformFont(
const char* fontID,
const char* fontName, ETextStyle style)
1577 const char* styles[] = {
"normal",
"bold",
"italic" };
1579 return PlatformFontPtr(
new Font(fontName, styles[
static_cast<int>(style)]));
1582PlatformFontPtr IGraphicsWeb::LoadPlatformFont(
const char* fontID,
void* pData,
int dataSize)
1584 return PlatformFontPtr(
new MemoryFont(fontID,
"", pData, dataSize));
1591void iGraphicsMouseCallback(
void* pGraphics,
int eventType,
double x,
double y,
double dx,
double dy,
int buttons,
int button,
int shift,
int ctrl,
int alt)
1594 float scale = pG->GetDrawScale();
1601 info.ms = {(buttons & 1) != 0, (buttons & 2) != 0,
static_cast<bool>(shift),
static_cast<bool>(ctrl),
static_cast<bool>(alt)};
1602 std::vector<IMouseInfo> list{info};
1607 pG->OnMouseDown(list);
1610 list[0].ms.L = button == 0;
1611 list[0].ms.R = button == 2;
1612 pG->OnMouseUp(list);
1616 pG->OnMouseOver(info.x, info.y, info.ms);
1617 else if (!pG->IsInPlatformTextEntry())
1618 pG->OnMouseDrag(list);
1622 pG->OnMouseOver(info.x, info.y, info.ms);
1629 pG->mPrevX = info.x;
1630 pG->mPrevY = info.y;
1634void iGraphicsWheelCallback(
void* pGraphics,
double x,
double y,
double deltaY,
int shift,
int ctrl,
int alt)
1637 float scale = pG->GetDrawScale();
1638 IMouseMod mod(
false,
false,
static_cast<bool>(shift),
static_cast<bool>(ctrl),
static_cast<bool>(alt));
1639 pG->OnMouseWheel(x / scale, y / scale, mod, deltaY);
1642#if !defined(NDEBUG) || defined(IPLUG_LIVE_EDIT)
1644int iplug_set_live_edit(
void* pGraphics,
int enabled)
1650 pG->EnableLiveEdit(enabled != 0);
1651 return pG->LiveEditEnabled() == (enabled != 0);
1657#if defined IGRAPHICS_NANOVG
1658#include "IGraphicsNanoVG.cpp"
1660#ifdef IGRAPHICS_FREETYPE
1661#define FONS_USE_FREETYPE
EResourceLocation LocateResource(const char *fileNameOrResID, const char *type, WDL_String &result, const char *bundleID, void *pHInstance, const char *sharedResourcesSubPath)
Find the absolute path of a resource based on it's file name (e.g.
An editor delegate base class that uses IGraphics for the UI.
The lowest level base class of an IGraphics context.
virtual ECursor SetMouseCursor(ECursor cursorType=ECursor::ARROW)
Sets the mouse cursor to one of ECursor (implementations should return the result of the base impleme...
void OnMouseDrag(const std::vector< IMouseInfo > &points)
Called when the platform class sends drag events.
void OnMouseUp(const std::vector< IMouseInfo > &points)
Called when the platform class sends mouse up events.
void OnTouchCancelled(const std::vector< IMouseInfo > &points)
Called when the platform class sends touch cancel events.
void OnMouseDown(const std::vector< IMouseInfo > &points)
Called when the platform class sends mouse down events.
bool OnMouseWheel(float x, float y, const IMouseMod &mod, float delta)
float GetDrawScale() const
Gets the graphics context scaling factor.
IGraphics platform class for the web.
IGraphicsWeb(IGEditorDelegate &dlg, int w, int h, int fps, float scale, val canvas=val::undefined())
Constructor.
val GetCanvas() const
Get the canvas element for this instance.
EParamType Type() const
Get the parameter's type.
Used to manage a list of rectangular areas and optimize them for drawing to the screen.
void Add(const IRECT &rect)
Add a rectangle to the list.
Used to group mouse coordinates with mouse modifier information.
int DOMKeyToVirtualKey(uint32_t domKeyCode)
Converts a DOM virtual key code to an iPlug2 virtual key code.
Used to manage color data, independent of draw class/platform.
Used for key press info, such as ASCII representation, virtual key (mapped to win32 codes) and modifi...
Used to manage mouse modifiers i.e.
Used to manage a rectangular area, independent of draw class/platform.
IText is used to manage font and text/text entry style for a piece of text on the UI,...