iPlug2 - C++ Audio Plug-in Framework
Loading...
Searching...
No Matches
IGraphicsWeb.cpp
1/*
2 ==============================================================================
3
4 This file is part of the iPlug 2 library. Copyright (C) the iPlug 2 developers.
5
6 See LICENSE.txt for more info.
7
8 ==============================================================================
9*/
10
11#include <cstring>
12#include <cstdio>
13#include <cstdint>
14#include <string>
15#include <vector>
16
17#include "IGraphicsWeb.h"
18
19EM_JS(int, iplug_wasm_capture_bridge_enabled, (), {
20 if (typeof window === "undefined") return 0;
21 try {
22 return new URLSearchParams(window.location.search).get("iplugWasmCapture") === "1";
23 } catch (e) {
24 return 0;
25 }
26});
27
28// Helper to create WebGL context for Shadow DOM (CSS selectors don't work).
29EM_JS(int, createWebGLContextForShadowDOM, (), {
30 var canvas = Module.canvas;
31 if (!canvas) return 0;
32 var preserveDrawingBuffer = false;
33 try {
34 preserveDrawingBuffer = new URLSearchParams(window.location.search).get("iplugWasmCapture") === "1";
35 } catch (e) {}
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);
38 if (!ctx) return 0;
39 return GL.registerContext(ctx, attrs);
40});
41
42EM_JS(void, iplug_popup_menu_show_js, (void* pGraphics, double viewportX, double viewportY, const char* menuJson), {
43 var rootItems;
44 var emitSelection = function(pG, path) {
45 if (!pG || !Module._iplug_popup_menu_selected) {
46 return;
47 }
48
49 if (Module.ccall) {
50 Module.ccall('iplug_popup_menu_selected', null, ['number', 'string'], [pG, path || ""]);
51 } else {
52 Module._iplug_popup_menu_selected(pG, 0);
53 }
54 };
55
56 try {
57 rootItems = JSON.parse(UTF8ToString(menuJson));
58 } catch (e) {
59 console.error('iPlug popup menu: invalid menu payload', e);
60 emitSelection(pGraphics, "");
61 return;
62 }
63
64 if (!HTMLElement.prototype.showPopover || !HTMLElement.prototype.hidePopover) {
65 emitSelection(pGraphics, "");
66 return;
67 }
68
69 var doc = document;
70 if (!doc.getElementById('__iplug_popup_menu_style')) {
71 var style = doc.createElement('style');
72 style.id = '__iplug_popup_menu_style';
73 style.textContent = [
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);}'
84 ].join('\n');
85 doc.head.appendChild(style);
86 }
87
88 var menu = doc.getElementById('__iplug_popup_menu');
89 if (!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');
96 // Popovers are promoted to the top layer, so appending to body still
97 // renders above canvases hosted inside Shadow DOM.
98 doc.body.appendChild(menu);
99 } else {
100 if (menu._iplugCleanup) {
101 menu._iplugCleanup(menu._iplugPGraphics && menu._iplugPGraphics !== pGraphics);
102 }
103 if (menu.matches && menu.matches(':popover-open') && menu.hidePopover) {
104 menu.hidePopover();
105 }
106 while (menu.firstChild) {
107 menu.removeChild(menu.firstChild);
108 }
109 }
110
111 menu._iplugPickedPath = "";
112 menu._iplugPGraphics = pGraphics;
113
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 + '"]');
119 if (trigger) {
120 trigger.setAttribute('aria-expanded', 'false');
121 }
122 }
123 panel.remove();
124 }
125 });
126 };
127
128 var getMenuItems = function(panel) {
129 if (!panel) {
130 return [];
131 }
132 return Array.prototype.slice.call(panel.querySelectorAll('.iplug-popup-menu__item:not(:disabled)'));
133 };
134
135 var focusButton = function(button) {
136 if (!button) {
137 return;
138 }
139 try {
140 button.focus({ preventScroll: true });
141 } catch (e) {
142 button.focus();
143 }
144 };
145
146 var focusMenuItem = function(panel, idx) {
147 var items = getMenuItems(panel);
148 if (!items.length) {
149 return;
150 }
151 var wrappedIdx = (idx + items.length) % items.length;
152 focusButton(items[wrappedIdx]);
153 };
154
155 var focusFirstMenuItem = function(panel) {
156 focusMenuItem(panel, 0);
157 };
158
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');
163 }
164
165 var panels = Array.prototype.slice.call(menu.querySelectorAll('.iplug-popup-menu__panel'));
166 return panels.length ? panels[panels.length - 1] : null;
167 };
168
169 var getActiveButton = function() {
170 var active = doc.activeElement;
171 if (active && active.classList && active.classList.contains('iplug-popup-menu__item')) {
172 return active;
173 }
174 return null;
175 };
176
177 var positionPanel = function(panel, x, y, fallbackRightEdge) {
178 var margin = 4;
179 panel.style.left = '0px';
180 panel.style.top = '0px';
181 panel.style.visibility = 'hidden';
182
183 var rect = panel.getBoundingClientRect();
184 var left = x;
185 var top = y;
186
187 if (left + rect.width + margin > window.innerWidth && fallbackRightEdge !== null) {
188 left = fallbackRightEdge - rect.width;
189 }
190
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));
193
194 panel.style.left = Math.round(left) + 'px';
195 panel.style.top = Math.round(top) + 'px';
196 panel.style.visibility = "";
197 };
198
199 var showPanel;
200 var appendMenuItem = function(panel, item, idx, path, depth) {
201 var childPath = path.concat([idx]);
202
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);
209 });
210 panel.appendChild(separator);
211 return;
212 }
213
214 if (item.title) {
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);
221 });
222 panel.appendChild(title);
223 return;
224 }
225
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);
233 if (item.checked) {
234 button.setAttribute('role', 'menuitemcheckbox');
235 button.setAttribute('aria-checked', 'true');
236 }
237
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);
242
243 var label = doc.createElement('span');
244 label.textContent = item.text || "";
245 button.appendChild(label);
246
247 if (hasSubmenu) {
248 button.setAttribute('aria-haspopup', 'menu');
249 button.setAttribute('aria-expanded', 'false');
250
251 var submenuIndicator = doc.createElement('span');
252 submenuIndicator.className = 'iplug-popup-menu__submenu-indicator';
253 submenuIndicator.textContent = String.fromCharCode(0x203a);
254 button.appendChild(submenuIndicator);
255
256 var openSubmenu = function(focusFirst) {
257 if (button.disabled) {
258 return;
259 }
260
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');
264 if (focusFirst) {
265 focusFirstMenuItem(childPanel);
266 }
267 };
268 button._iplugOpenSubmenu = openSubmenu;
269
270 button.addEventListener('pointerenter', function() {
271 openSubmenu(false);
272 });
273 button.addEventListener('click', function(e) {
274 e.preventDefault();
275 openSubmenu(false);
276 });
277 } else {
278 button.addEventListener('pointerenter', function() {
279 clearPanelsFrom(depth + 1);
280 });
281 button.addEventListener('click', function() {
282 if (button.disabled) {
283 return;
284 }
285 menu._iplugPickedPath = childPath.join(',');
286 menu.hidePopover();
287 });
288 }
289
290 panel.appendChild(button);
291 };
292
293 showPanel = function(items, path, depth, x, y, fallbackRightEdge) {
294 clearPanelsFrom(depth);
295
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');
301
302 items.forEach(function(item, idx) {
303 appendMenuItem(panel, item, idx, path, depth);
304 });
305
306 menu.appendChild(panel);
307 positionPanel(panel, x, y, fallbackRightEdge);
308 return panel;
309 };
310
311 var onDocPointerDown = function(e) {
312 if (menu.contains(e.target)) {
313 return;
314 }
315 menu.hidePopover();
316 };
317
318 var moveFocus = function(delta) {
319 var panel = getActivePanel();
320 var items = getMenuItems(panel);
321 if (!items.length) {
322 return;
323 }
324
325 var idx = items.indexOf(getActiveButton());
326 if (idx < 0) {
327 focusButton(items[delta > 0 ? 0 : items.length - 1]);
328 } else {
329 focusButton(items[(idx + delta + items.length) % items.length]);
330 }
331 };
332
333 var closeActiveSubmenu = function() {
334 var panel = getActivePanel();
335 if (!panel) {
336 return false;
337 }
338
339 var depth = Number(panel.dataset.depth);
340 if (depth <= 0) {
341 return false;
342 }
343
344 var path = panel.dataset.path || "";
345 clearPanelsFrom(depth);
346 focusButton(menu.querySelector('.iplug-popup-menu__item[data-iplug-path="' + path + '"]'));
347 return true;
348 };
349
350 var onKeyDown = function(e) {
351 var handled = true;
352 var activeButton = getActiveButton();
353
354 switch (e.key) {
355 case 'Escape':
356 menu.hidePopover();
357 break;
358 case 'ArrowDown':
359 moveFocus(1);
360 break;
361 case 'ArrowUp':
362 moveFocus(-1);
363 break;
364 case 'Home':
365 focusMenuItem(getActivePanel(), 0);
366 break;
367 case 'End':
368 var panel = getActivePanel();
369 focusMenuItem(panel, getMenuItems(panel).length - 1);
370 break;
371 case 'ArrowRight':
372 if (activeButton && activeButton._iplugOpenSubmenu) {
373 activeButton._iplugOpenSubmenu(true);
374 }
375 break;
376 case 'ArrowLeft':
377 closeActiveSubmenu();
378 break;
379 case 'Enter':
380 case ' ':
381 case 'Spacebar':
382 if (activeButton) {
383 if (activeButton._iplugOpenSubmenu) {
384 activeButton._iplugOpenSubmenu(true);
385 } else {
386 activeButton.click();
387 }
388 }
389 break;
390 default:
391 handled = false;
392 break;
393 }
394
395 if (handled) {
396 e.preventDefault();
397 e.stopPropagation();
398 }
399 };
400
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;
406
407 var picked = menu._iplugPickedPath;
408 var pG = menu._iplugPGraphics;
409 menu._iplugPickedPath = "";
410 menu._iplugPGraphics = null;
411
412 if (sendCallback) {
413 emitSelection(pG, picked);
414 }
415 };
416
417 var onToggle = function(e) {
418 if (e.newState === 'closed') {
419 finish(true);
420 }
421 };
422
423 menu._iplugCleanup = finish;
424 menu.addEventListener('toggle', onToggle);
425
426 setTimeout(function() {
427 if (menu._iplugCleanup !== finish || menu._iplugPGraphics !== pGraphics) {
428 return;
429 }
430
431 try {
432 menu.showPopover();
433 } catch (e) {
434 console.warn('iPlug popup menu: showPopover failed', e);
435 finish(true);
436 return;
437 }
438
439 showPanel(rootItems, [], 0, viewportX, viewportY, null);
440
441 try {
442 menu.focus({ preventScroll: true });
443 } catch (e) {
444 menu.focus();
445 }
446
447 setTimeout(function() {
448 if (menu._iplugCleanup === finish && menu._iplugPGraphics === pGraphics) {
449 doc.addEventListener('pointerdown', onDocPointerDown, true);
450 doc.addEventListener('keydown', onKeyDown, true);
451 }
452 }, 0);
453 }, 0);
454});
455
456EM_JS(void, iplug_popup_menu_close_js, (void* pGraphics), {
457 var menu = document.getElementById('__iplug_popup_menu');
458 if (!menu || menu._iplugPGraphics !== pGraphics) {
459 return;
460 }
461
462 menu._iplugPGraphics = null;
463 menu._iplugPickedPath = "";
464 if (menu.matches && menu.matches(':popover-open') && menu.hidePopover) {
465 menu.hidePopover();
466 } else if (menu._iplugCleanup) {
467 menu._iplugCleanup(false);
468 }
469});
470
471#if !defined(NDEBUG) || defined(IPLUG_LIVE_EDIT)
472EM_JS(void, iplug_emit_live_edit_js, (const char* eventJson), {
473 var msg;
474 try {
475 msg = JSON.parse(UTF8ToString(eventJson));
476 } catch (e) {
477 console.error('iPlug live edit: invalid event payload', e);
478 return;
479 }
480
481 try {
482 // Live edit is an opt-in developer channel. The wildcard origin lets
483 // file:// previews and IDE shells host the frame without build-time
484 // origin knowledge; do not enable IPLUG2_WASM_LIVE_EDIT for production.
485 window.postMessage(msg, '*');
486 if (window.parent && window.parent !== window) {
487 window.parent.postMessage(msg, '*');
488 }
489 } catch (e) {
490 console.warn('iPlug live edit: postMessage failed', e);
491 }
492});
493#endif
494
495
496BEGIN_IPLUG_NAMESPACE
497BEGIN_IGRAPHICS_NAMESPACE
498
499void GetScreenDimensions(int& width, int& height)
500{
501 width = val::global("window")["innerWidth"].as<int>();
502 height = val::global("window")["innerHeight"].as<int>();
503}
504
505END_IPLUG_NAMESPACE
506END_IGRAPHICS_NAMESPACE
507
508using namespace iplug;
509using namespace igraphics;
510using namespace emscripten;
511
512extern std::vector<IGraphicsWeb*> gGraphicsInstances;
513extern void UnregisterGraphicsInstance(IGraphicsWeb* pGraphics);
514double gPrevMouseDownTime = 0.;
515bool gFirstClick = false;
516
517#pragma mark - Private Classes and Structs
518
519// Fonts
520
521class IGraphicsWeb::Font : public PlatformFont
522{
523public:
524 Font(const char* fontName, const char* fontStyle)
525 : PlatformFont(true), mDescriptor{fontName, fontStyle}
526 {}
527
528 FontDescriptor GetDescriptor() override { return &mDescriptor; }
529
530private:
531 std::pair<WDL_String, WDL_String> mDescriptor;
532};
533
534class IGraphicsWeb::FileFont : public Font
535{
536public:
537 FileFont(const char* fontName, const char* fontStyle, const char* fontPath)
538 : Font(fontName, fontStyle), mPath(fontPath)
539 {
540 mSystem = false;
541 }
542
543 IFontDataPtr GetFontData() override;
544
545private:
546 WDL_String mPath;
547};
548
549IFontDataPtr IGraphicsWeb::FileFont::GetFontData()
550{
551 IFontDataPtr fontData(new IFontData());
552 FILE* fp = fopen(mPath.Get(), "rb");
553
554 // Read in the font data.
555 if (!fp)
556 return fontData;
557
558 fseek(fp,0,SEEK_END);
559 fontData = std::make_unique<IFontData>((int) ftell(fp));
560
561 if (!fontData->GetSize())
562 return fontData;
563
564 fseek(fp,0,SEEK_SET);
565 size_t readSize = fread(fontData->Get(), 1, fontData->GetSize(), fp);
566 fclose(fp);
567
568 if (readSize && readSize == fontData->GetSize())
569 fontData->SetFaceIdx(0);
570
571 return fontData;
572}
573
574class IGraphicsWeb::MemoryFont : public Font
575{
576public:
577 MemoryFont(const char* fontName, const char* fontStyle, const void* pData, int dataSize)
578 : Font(fontName, fontStyle)
579 {
580 mSystem = false;
581 mData.Set((const uint8_t*)pData, dataSize);
582 }
583
584 IFontDataPtr GetFontData() override
585 {
586 return IFontDataPtr(new IFontData(mData.Get(), mData.GetSize(), 0));
587 }
588
589private:
590 WDL_TypedBuf<uint8_t> mData;
591};
592
593#pragma mark - Utilities and Callbacks
594
595static EM_BOOL key_callback(int eventType, const EmscriptenKeyboardEvent* pEvent, void* pUserData)
596{
597 IGraphicsWeb* pGraphicsWeb = (IGraphicsWeb*) pUserData;
598
599 int VK = DOMKeyToVirtualKey(pEvent->keyCode);
600 WDL_String keyUTF8;
601
602 // filter utf8 for non ascii keys
603 if ((VK >= kVK_0 && VK <= kVK_Z) || VK == kVK_NONE)
604 keyUTF8.Set(pEvent->key);
605 else
606 keyUTF8.Set("");
607
608 IKeyPress keyPress {keyUTF8.Get(),
609 DOMKeyToVirtualKey(pEvent->keyCode),
610 static_cast<bool>(pEvent->shiftKey),
611 static_cast<bool>(pEvent->ctrlKey || pEvent->metaKey),
612 static_cast<bool>(pEvent->altKey)};
613
614 switch (eventType)
615 {
616 case EMSCRIPTEN_EVENT_KEYDOWN:
617 {
618 return pGraphicsWeb->OnKeyDown(pGraphicsWeb->mPrevX, pGraphicsWeb->mPrevY, keyPress);
619 }
620 case EMSCRIPTEN_EVENT_KEYUP:
621 {
622 return pGraphicsWeb->OnKeyUp(pGraphicsWeb->mPrevX, pGraphicsWeb->mPrevY, keyPress);
623 }
624 default:
625 break;
626 }
627
628 return 0;
629}
630
631static EM_BOOL outside_mouse_callback(int eventType, const EmscriptenMouseEvent* pEvent, void* pUserData)
632{
633 IGraphicsWeb* pGraphics = (IGraphicsWeb*) pUserData;
634
635 IMouseInfo info;
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};
643
644 switch (eventType)
645 {
646 case EMSCRIPTEN_EVENT_MOUSEUP:
647 {
648 // Get button states based on what caused the mouse up (nothing in buttons)
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);
654 break;
655 }
656 case EMSCRIPTEN_EVENT_MOUSEMOVE:
657 {
658 if(pEvent->buttons != 0 && !pGraphics->IsInPlatformTextEntry())
659 pGraphics->OnMouseDrag(list);
660 break;
661 }
662 default:
663 break;
664 }
665
666 pGraphics->mPrevX = info.x;
667 pGraphics->mPrevY = info.y;
668
669 return true;
670}
671
672static EM_BOOL mouse_callback(int eventType, const EmscriptenMouseEvent* pEvent, void* pUserData)
673{
674 IGraphicsWeb* pGraphics = (IGraphicsWeb*) pUserData;
675
676 IMouseInfo info;
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)};
686
687 std::vector<IMouseInfo> list {info};
688 switch (eventType)
689 {
690 case EMSCRIPTEN_EVENT_MOUSEDOWN:
691 {
692 const double timestamp = GetTimestamp();
693 const double timeDiff = timestamp - gPrevMouseDownTime;
694
695 if (gFirstClick && timeDiff < 0.3)
696 {
697 gFirstClick = false;
698 pGraphics->OnMouseDblClick(info.x, info.y, info.ms);
699 }
700 else
701 {
702 gFirstClick = true;
703 pGraphics->OnMouseDown(list);
704 }
705
706 gPrevMouseDownTime = timestamp;
707
708 break;
709 }
710 case EMSCRIPTEN_EVENT_MOUSEUP:
711 {
712 // Get button states based on what caused the mouse up (nothing in buttons)
713 list[0].ms.L = pEvent->button == 0;
714 list[0].ms.R = pEvent->button == 2;
715 pGraphics->OnMouseUp(list);
716 break;
717 }
718 case EMSCRIPTEN_EVENT_MOUSEMOVE:
719 {
720 gFirstClick = false;
721
722 if(pEvent->buttons == 0)
723 pGraphics->OnMouseOver(info.x, info.y, info.ms);
724 else
725 {
726 if(!pGraphics->IsInPlatformTextEntry())
727 pGraphics->OnMouseDrag(list);
728 }
729 break;
730 }
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);
735 break;
736 case EMSCRIPTEN_EVENT_MOUSELEAVE:
737 if(pEvent->buttons != 0)
738 {
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);
741 }
742 pGraphics->OnMouseOut(); break;
743 default:
744 break;
745 }
746
747 pGraphics->mPrevX = info.x;
748 pGraphics->mPrevY = info.y;
749
750 return true;
751}
752
753static EM_BOOL wheel_callback(int eventType, const EmscriptenWheelEvent* pEvent, void* pUserData)
754{
755 IGraphics* pGraphics = (IGraphics*) pUserData;
756
757 IMouseMod modifiers(false, false, pEvent->mouse.shiftKey, pEvent->mouse.ctrlKey, pEvent->mouse.altKey);
758
759 double x = pEvent->mouse.targetX;
760 double y = pEvent->mouse.targetY;
761
762 x /= pGraphics->GetDrawScale();
763 y /= pGraphics->GetDrawScale();
764
765 switch (eventType) {
766 case EMSCRIPTEN_EVENT_WHEEL: pGraphics->OnMouseWheel(x, y, modifiers, pEvent->deltaY);
767 default:
768 break;
769 }
770
771 return true;
772}
773
774EM_BOOL touch_callback(int eventType, const EmscriptenTouchEvent* pEvent, void* pUserData)
775{
776 IGraphics* pGraphics = (IGraphics*) pUserData;
777 const float drawScale = pGraphics->GetDrawScale();
778
779 std::vector<IMouseInfo> points;
780
781 static EmscriptenTouchPoint previousTouches[32];
782
783 for (auto i = 0; i < pEvent->numTouches; i++)
784 {
785 IMouseInfo info;
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);
790 info.ms = {true,
791 false,
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)
796 };
797
798 if(pEvent->touches[i].isChanged)
799 points.push_back(info);
800 }
801
802 memcpy(previousTouches, pEvent->touches, sizeof(previousTouches));
803
804 switch (eventType)
805 {
806 case EMSCRIPTEN_EVENT_TOUCHSTART:
807 pGraphics->OnMouseDown(points);
808 return true;
809 case EMSCRIPTEN_EVENT_TOUCHEND:
810 pGraphics->OnMouseUp(points);
811 return true;
812 case EMSCRIPTEN_EVENT_TOUCHMOVE:
813 pGraphics->OnMouseDrag(points);
814 return true;
815 case EMSCRIPTEN_EVENT_TOUCHCANCEL:
816 pGraphics->OnTouchCancelled(points);
817 return true;
818 default:
819 return false;
820 }
821}
822
823static EM_BOOL complete_text_entry(int eventType, const EmscriptenFocusEvent* focusEvent, void* pUserData)
824{
825 IGraphicsWeb* pGraphics = (IGraphicsWeb*) pUserData;
826
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());
831
832 return true;
833}
834
835static EM_BOOL text_entry_keydown(int eventType, const EmscriptenKeyboardEvent* pEvent, void* pUserData)
836{
837 IGraphicsWeb* pGraphicsWeb = (IGraphicsWeb*) pUserData;
838
839 IKeyPress keyPress {pEvent->key, DOMKeyToVirtualKey(pEvent->keyCode),
840 static_cast<bool>(pEvent->shiftKey),
841 static_cast<bool>(pEvent->ctrlKey),
842 static_cast<bool>(pEvent->altKey)};
843
844 if (keyPress.VK == kVK_RETURN || keyPress.VK == kVK_TAB)
845 return complete_text_entry(0, nullptr, pUserData);
846
847 return false;
848}
849
850static EM_BOOL uievent_callback(int eventType, const EmscriptenUiEvent* pEvent, void* pUserData)
851{
852 IGraphicsWeb* pGraphics = (IGraphicsWeb*) pUserData;
853
854 if (eventType == EMSCRIPTEN_EVENT_RESIZE)
855 {
856 pGraphics->GetDelegate()->OnParentWindowResize(pEvent->windowInnerWidth, pEvent->windowInnerHeight);
857
858 return true;
859 }
860
861 return false;
862}
863
864IColorPickerHandlerFunc gColorPickerHandlerFunc = nullptr;
865
866static void color_picker_callback(val e)
867{
868 if(gColorPickerHandlerFunc)
869 {
870 std::string colorStrHex = e["target"]["value"].as<std::string>();
871
872 if (colorStrHex[0] == '#')
873 colorStrHex = colorStrHex.erase(0, 1);
874
875 IColor result;
876 result.A = 255;
877 sscanf(colorStrHex.c_str(), "%02x%02x%02x", &result.R, &result.G, &result.B);
878
879 gColorPickerHandlerFunc(result);
880 }
881}
882
883static void file_dialog_callback(val e)
884{
885 // DBGMSG(e["files"].as<std::string>().c_str());
886}
887
888EMSCRIPTEN_BINDINGS(events) {
889 function("color_picker_callback", color_picker_callback);
890 function("file_dialog_callback", file_dialog_callback);
891}
892
893#pragma mark -
894
895IGraphicsWeb::IGraphicsWeb(IGEditorDelegate& dlg, int w, int h, int fps, float scale, val canvas)
896: IGRAPHICS_DRAW_CLASS(dlg, w, h, fps, scale)
897{
898#if !defined(NDEBUG) || defined(IPLUG_LIVE_EDIT)
899 SetLiveEditEventFunc([](const char* eventJson) {
900 iplug_emit_live_edit_js(eventJson);
901 });
902#endif
903
904 val keys = val::global("Object").call<val>("keys", GetPreloadedImages());
905
906 DBGMSG("Preloaded %i images\n", keys["length"].as<int>());
907
908 // Initialize canvas - use provided element, Module.canvas, or fall back to getElementById
909 if (canvas.isUndefined() || canvas.isNull())
910 {
911 // Try Module.canvas first (set by web component or HTML template)
912 val moduleCanvas = val::global("Module")["canvas"];
913 if (!moduleCanvas.isUndefined() && !moduleCanvas.isNull())
914 {
915 mCanvas = moduleCanvas;
916 }
917 else
918 {
919 // Fall back to getElementById for legacy templates
920 mCanvas = val::global("document").call<val>("getElementById", std::string("canvas"));
921 }
922 }
923 else
924 {
925 mCanvas = canvas;
926 }
927
928 // Detect Shadow DOM by checking the canvas's root node
929 mRootNode = mCanvas.call<val>("getRootNode");
930 std::string rootNodeType = mRootNode["constructor"]["name"].as<std::string>();
931 mInShadowDOM = (rootNodeType == "ShadowRoot");
932
933 // Use the canvas's existing id if it has one (legacy templates rely on
934 // querying their <canvas id="canvas"> from JS); otherwise synthesize a
935 // per-instance id so multiple instances on a page don't collide.
936 std::string existingId = mCanvas["id"].as<std::string>();
937 if (existingId.empty())
938 {
939 char idBuf[64];
940 snprintf(idBuf, sizeof(idBuf), "iplug-canvas-%p", static_cast<void*>(this));
941 existingId = idBuf;
942 mCanvas.set("id", existingId);
943 }
944 mCanvasSelector = std::string("#") + existingId;
945
946 DBGMSG("IGraphicsWeb: Shadow DOM = %s, selector = %s\n", mInShadowDOM ? "true" : "false", mCanvasSelector.c_str());
947
948 RegisterCanvasEvents();
949}
950
951void IGraphicsWeb::RegisterCanvasEvents()
952{
953 // Window-level events always work
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);
957
958 if (mInShadowDOM)
959 {
960 // Shadow DOM: emscripten's CSS selector-based callbacks don't work
961 // Set up events via JavaScript on the canvas element directly
962 EM_ASM({
963 var pGraphics = $0;
964 var canvas = Module.canvas;
965 if (!canvas) return;
966
967 // Store reference for cleanup
968 canvas._iplugGraphics = pGraphics;
969
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);
972 });
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);
975 });
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);
978 });
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);
981 });
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);
984 });
985 canvas.addEventListener('wheel', function(e) {
986 Module._iGraphicsWheelCallback(pGraphics, e.offsetX, e.offsetY, e.deltaY, e.shiftKey, e.ctrlKey, e.altKey);
987 e.preventDefault();
988 }, { passive: false });
989 // TODO: Touch events for Shadow DOM
990 }, this);
991 }
992 else
993 {
994 // Regular DOM: use emscripten's callback system
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);
1006 }
1007}
1008
1009void IGraphicsWeb::UnregisterCanvasEvents()
1010{
1011 const char* target = mCanvasSelector.c_str();
1012
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);
1025}
1026
1027IGraphicsWeb::~IGraphicsWeb()
1028{
1029 iplug_popup_menu_close_js(this);
1030 UnregisterCanvasEvents();
1031 UnregisterGraphicsInstance(this);
1032}
1033
1034void* IGraphicsWeb::OpenWindow(void* pHandle)
1035{
1036#ifdef IGRAPHICS_GL
1037 EmscriptenWebGLContextAttributes attr;
1038 emscripten_webgl_init_context_attributes(&attr);
1039 attr.stencil = true;
1040 attr.depth = true;
1041 attr.antialias = true;
1042 attr.alpha = true;
1043 // Capture is URL opt-in because preserveDrawingBuffer can reduce WebGL
1044 // presentation throughput in regular production plugin embeds.
1045 attr.preserveDrawingBuffer = iplug_wasm_capture_bridge_enabled();
1046// attr.explicitSwapControl = 1;
1047
1048 EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx;
1049
1050 if (mInShadowDOM)
1051 {
1052 // Shadow DOM: create context via JS since CSS selectors don't work
1053 ctx = createWebGLContextForShadowDOM();
1054 }
1055 else
1056 {
1057 // Regular DOM: use standard emscripten API
1058 ctx = emscripten_webgl_create_context(mCanvasSelector.c_str(), &attr);
1059 }
1060
1061 emscripten_webgl_make_context_current(ctx);
1062#endif
1063
1064 OnViewInitialized(nullptr /* not used */);
1065
1066 SetScreenScale(std::ceil(std::max(emscripten_get_device_pixel_ratio(), 1.)));
1067
1068 GetDelegate()->LayoutUI(this);
1069 GetDelegate()->OnUIOpen();
1070
1071 return nullptr;
1072}
1073
1074void IGraphicsWeb::HideMouseCursor(bool hide, bool lock)
1075{
1076 if (mCursorHidden == hide)
1077 return;
1078
1079 if (hide)
1080 {
1081#ifdef IGRAPHICS_WEB_POINTERLOCK
1082 if (lock)
1083 emscripten_request_pointerlock(mCanvasSelector.c_str(), EM_FALSE);
1084 else
1085#endif
1086 mCanvas["style"].set("cursor", "none");
1087
1088 mCursorHidden = true;
1089 mCursorLock = lock;
1090 }
1091 else
1092 {
1093#ifdef IGRAPHICS_WEB_POINTERLOCK
1094 if (mCursorLock)
1095 emscripten_exit_pointerlock();
1096 else
1097#endif
1098 OnSetCursor();
1099
1100 mCursorHidden = false;
1101 mCursorLock = false;
1102 }
1103}
1104
1105ECursor IGraphicsWeb::SetMouseCursor(ECursor cursorType)
1106{
1107 std::string cursor("pointer");
1108
1109 switch (cursorType)
1110 {
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;
1125 }
1126
1127 mCanvas["style"].set("cursor", cursor);
1128 return IGraphics::SetMouseCursor(cursorType);
1129}
1130
1131void IGraphicsWeb::GetMouseLocation(float& x, float&y) const
1132{
1133 x = mPrevX;
1134 y = mPrevY;
1135}
1136
1137//static
1138void IGraphicsWeb::OnMainLoopTimer()
1139{
1140 int screenScale = (int) std::ceil(std::max(emscripten_get_device_pixel_ratio(), 1.));
1141
1142 // Iterate over all registered graphics instances
1143 for (IGraphicsWeb* pGraphics : gGraphicsInstances)
1144 {
1145 if (pGraphics == nullptr)
1146 continue;
1147
1148 if (screenScale != pGraphics->GetScreenScale())
1149 {
1150 pGraphics->SetScreenScale(screenScale);
1151 }
1152
1153 IRECTList rects;
1154 if (pGraphics->IsDirty(rects))
1155 {
1156 pGraphics->SetAllControlsClean();
1157 pGraphics->Draw(rects);
1158 }
1159 }
1160}
1161
1162EMsgBoxResult IGraphicsWeb::ShowMessageBox(const char* str, const char* /*title*/, EMsgBoxType type, IMsgBoxCompletionHandlerFunc completionHandler)
1163{
1164 ReleaseMouseCapture();
1165
1166 EMsgBoxResult result = kNoResult;
1167
1168 switch (type)
1169 {
1170 case kMB_OK:
1171 {
1172 val::global("window").call<val>("alert", std::string(str));
1173 result = EMsgBoxResult::kOK;
1174 break;
1175 }
1176 case kMB_YESNO:
1177 case kMB_OKCANCEL:
1178 {
1179 result = static_cast<EMsgBoxResult>(val::global("window").call<val>("confirm", std::string(str)).as<int>());
1180 }
1181 // case MB_CANCEL:
1182 // break;
1183 default:
1184 return result = kNoResult;
1185 }
1186
1187 if(completionHandler)
1188 completionHandler(result);
1189
1190 return result;
1191}
1192
1193void IGraphicsWeb::PromptForFile(WDL_String& filename, WDL_String& path, EFileAction action, const char* ext, IFileDialogCompletionHandlerFunc completionHandler)
1194{
1195 //TODO
1196 // val inputEl = val::global("document").call<val>("createElement", std::string("input"));
1197
1198 // inputEl.call<void>("setAttribute", std::string("type"), std::string("file"));
1199 // inputEl.call<void>("setAttribute", std::string("accept"), std::string(ext));
1200 // inputEl.call<void>("click");
1201 // inputEl.call<void>("addEventListener", std::string("input"), val::module_property("file_dialog_callback"), false);
1202 // inputEl.call<void>("addEventListener", std::string("onChange"), val::module_property("file_dialog_callback"), false);
1203}
1204
1205void IGraphicsWeb::PromptForDirectory(WDL_String& path, IFileDialogCompletionHandlerFunc completionHandler)
1206{
1207 //TODO
1208 // val inputEl = val::global("document").call<val>("createElement", std::string("input"));
1209
1210 // inputEl.call<void>("setAttribute", std::string("type"), std::string("file"));
1211 // inputEl.call<void>("setAttribute", std::string("directory"), true);
1212 // inputEl.call<void>("setAttribute", std::string("webkitdirectory"), true);
1213 // inputEl.call<void>("click");
1214 // inputEl.call<void>("addEventListener", std::string("input"), val::module_property("file_dialog_callback"), false);
1215 // inputEl.call<void>("addEventListener", std::string("onChange"), val::module_property("file_dialog_callback"), false);
1216}
1217
1218bool IGraphicsWeb::PromptForColor(IColor& color, const char* str, IColorPickerHandlerFunc func)
1219{
1220 ReleaseMouseCapture();
1221
1222 gColorPickerHandlerFunc = func;
1223
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);
1232
1233 return false;
1234}
1235
1236void IGraphicsWeb::CreatePlatformTextEntry(int paramIdx, const IText& text, const IRECT& bounds, int length, const char* str)
1237{
1238 val input = val::global("document").call<val>("createElement", std::string("input"));
1239 const val rect = mCanvas.call<val>("getBoundingClientRect");
1240
1241 auto setDim = [&input](const char *dimName, double pixels)
1242 {
1243 WDL_String dimstr;
1244 dimstr.SetFormatted(32, "%fpx", pixels);
1245 input["style"].set(dimName, std::string(dimstr.Get()));
1246 };
1247
1248 auto setColor = [&input](const char *colorName, IColor color)
1249 {
1250 WDL_String str;
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()));
1253 };
1254
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());
1261
1262 setColor("color", text.mTextEntryFGColor);
1263 setColor("background-color", text.mTextEntryBGColor);
1264 if (paramIdx > kNoParameter)
1265 {
1266 const IParam* pParam = GetDelegate()->GetParam(paramIdx);
1267
1268 switch (pParam->Type())
1269 {
1270 case IParam::kTypeEnum:
1271 case IParam::kTypeInt:
1272 case IParam::kTypeBool:
1273 input.set("type", val("number")); // TODO
1274 break;
1275 case IParam::kTypeDouble:
1276 input.set("type", val("number"));
1277 break;
1278 default:
1279 break;
1280 }
1281 }
1282 else
1283 {
1284 input.set("type", val("text"));
1285 }
1286
1287 // Append to shadow root or document.body based on mode
1288 if (mInShadowDOM)
1289 {
1290 mRootNode.call<void>("appendChild", input);
1291 }
1292 else
1293 {
1294 val::global("document")["body"].call<void>("appendChild", input);
1295 }
1296
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);
1300}
1301
1302namespace
1303{
1304 void AppendJsonString(std::string& out, const char* str)
1305 {
1306 out.push_back('"');
1307
1308 if (!str)
1309 {
1310 out.push_back('"');
1311 return;
1312 }
1313
1314 for (const char* p = str; *p; ++p)
1315 {
1316 const unsigned char c = static_cast<unsigned char>(*p);
1317
1318 switch (c)
1319 {
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;
1327 default:
1328 if (c < 0x20)
1329 {
1330 char esc[8];
1331 std::snprintf(esc, sizeof(esc), "\\u%04x", c);
1332 out += esc;
1333 }
1334 else
1335 {
1336 out.push_back(static_cast<char>(c));
1337 }
1338 break;
1339 }
1340 }
1341
1342 out.push_back('"');
1343 }
1344
1345 std::string GetPopupMenuItemText(IPopupMenu& menu, int itemIdx, IPopupMenu::Item& item)
1346 {
1347 const char* itemText = item.GetText();
1348
1349 if (!menu.GetPrefix() || item.GetIsSeparator())
1350 return itemText ? itemText : "";
1351
1352 char prefix[16];
1353 switch (menu.GetPrefix())
1354 {
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;
1359 }
1360
1361 return std::string(prefix) + (itemText ? itemText : "");
1362 }
1363
1364 void AppendPopupMenuJson(std::string& json, IPopupMenu& menu)
1365 {
1366 json.push_back('[');
1367
1368 for (int i = 0; i < menu.NItems(); ++i)
1369 {
1370 IPopupMenu::Item* pItem = menu.GetItem(i);
1371 if (i > 0)
1372 json.push_back(',');
1373
1374 json.push_back('{');
1375 json += "\"text\":";
1376 std::string itemText;
1377
1378 if (pItem)
1379 itemText = GetPopupMenuItemText(menu, i, *pItem);
1380
1381 AppendJsonString(json, itemText.c_str());
1382
1383 if (pItem)
1384 {
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";
1389
1390 if (IPopupMenu* pSubmenu = pItem->GetSubmenu())
1391 {
1392 json += ",\"submenu\":";
1393 AppendPopupMenuJson(json, *pSubmenu);
1394 }
1395 }
1396
1397 json.push_back('}');
1398 }
1399
1400 json.push_back(']');
1401 }
1402
1403 bool ReadPopupMenuPathIndex(const char*& path, int& idx)
1404 {
1405 if (!path || *path < '0' || *path > '9')
1406 return false;
1407
1408 idx = 0;
1409 while (*path >= '0' && *path <= '9')
1410 {
1411 idx = (idx * 10) + (*path - '0');
1412 ++path;
1413 }
1414
1415 return true;
1416 }
1417
1418 bool ResolvePopupMenuPath(IPopupMenu& rootMenu, const char* path, IPopupMenu*& pSelectedMenu, int& selectedIdx)
1419 {
1420 if (!path || !*path)
1421 return false;
1422
1423 IPopupMenu* pMenu = &rootMenu;
1424 const char* p = path;
1425
1426 while (*p)
1427 {
1428 int idx = -1;
1429 if (!ReadPopupMenuPathIndex(p, idx))
1430 return false;
1431
1432 IPopupMenu::Item* pItem = pMenu->GetItem(idx);
1433 if (!pItem)
1434 return false;
1435
1436 if (*p == ',')
1437 {
1438 ++p;
1439 pMenu = pItem->GetSubmenu();
1440 if (!pMenu || !*p)
1441 return false;
1442
1443 continue;
1444 }
1445
1446 if (*p != '\0')
1447 return false;
1448
1449 pSelectedMenu = pMenu;
1450 selectedIdx = idx;
1451 return true;
1452 }
1453
1454 return false;
1455 }
1456}
1457
1458IPopupMenu* IGraphicsWeb::CreatePlatformPopupMenu(IPopupMenu& menu, const IRECT bounds, bool& isAsync)
1459{
1460 isAsync = true;
1461 mCurrentPopupMenu = &menu;
1462 menu.SetChosenItemIdx(-1);
1463
1464 std::string json;
1465 json.reserve(64 + (menu.NItems() * 48));
1466 AppendPopupMenuJson(json, menu);
1467
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());
1473
1474 return nullptr;
1475}
1476
1477void IGraphicsWeb::OnPopupMenuSelectedAsync(const char* path)
1478{
1479 IPopupMenu* pRootMenu = mCurrentPopupMenu;
1480 mCurrentPopupMenu = nullptr;
1481
1482 if (!pRootMenu)
1483 return;
1484
1485 IPopupMenu* pMenu = nullptr;
1486 int idx = -1;
1487 if (!ResolvePopupMenuPath(*pRootMenu, path, pMenu, idx))
1488 {
1489 SetControlValueAfterPopupMenu(nullptr);
1490 return;
1491 }
1492
1493 IPopupMenu::Item* pItem = pMenu->GetItem(idx);
1494 if (pItem && pItem->GetIsChoosable())
1495 {
1496 pMenu->SetChosenItemIdx(idx);
1497
1498 if (pMenu->GetFunction())
1499 pMenu->ExecFunction();
1500
1501 SetControlValueAfterPopupMenu(pMenu);
1502 }
1503 else
1504 {
1505 SetControlValueAfterPopupMenu(nullptr);
1506 }
1507}
1508
1509extern "C" EMSCRIPTEN_KEEPALIVE
1510void iplug_popup_menu_selected(void* pGraphics, const char* path)
1511{
1512 if (pGraphics)
1513 static_cast<IGraphicsWeb*>(pGraphics)->OnPopupMenuSelectedAsync(path);
1514}
1515
1516bool IGraphicsWeb::OpenURL(const char* url, const char* msgWindowTitle, const char* confirmMsg, const char* errMsgOnFailure)
1517{
1518 val::global("window").call<val>("open", std::string(url), std::string("_blank"));
1519
1520 return true;
1521}
1522
1523void IGraphicsWeb::DrawResize()
1524{
1525 // CSS style.width/height need "px" suffix
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));
1530
1531 // Canvas element width/height attributes are integers (no px).
1532 // Assigning these clears the WebGL drawing buffer even when the value
1533 // is unchanged, so guard against redundant writes — otherwise every
1534 // ResizeObserver tick during a drag causes a visible flash.
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)
1540 {
1541 mCanvas.set("width", newBufW);
1542 mCanvas.set("height", newBufH);
1543 }
1544
1545 IGRAPHICS_DRAW_CLASS::DrawResize();
1546}
1547
1548void IGraphicsWeb::PostResize()
1549{
1550 // Called at the end of IGraphics::Resize(), after OnResize +
1551 // SetAllControlsDirty + DrawResize + (optional) LayoutUI have all
1552 // run. At this point control layout is final, so it's safe to
1553 // repaint synchronously. Without this, the canvas is stuck in a
1554 // cleared state (canvas.width/height assignment wiped the default
1555 // framebuffer; NanoVG's DrawResize rebuilt an empty FBO) until the
1556 // next main-loop RAF tick, which the browser may composite past —
1557 // producing a one-frame blank flash on every size change.
1558 IRECTList rects;
1559 rects.Add(GetBounds());
1560 SetAllControlsClean();
1561 Draw(rects);
1562}
1563
1564PlatformFontPtr IGraphicsWeb::LoadPlatformFont(const char* fontID, const char* fileNameOrResID)
1565{
1566 WDL_String fullPath;
1567 const EResourceLocation fontLocation = LocateResource(fileNameOrResID, "ttf", fullPath, GetBundleID(), nullptr, nullptr);
1568
1569 if (fontLocation == kNotFound)
1570 return nullptr;
1571
1572 return PlatformFontPtr(new FileFont(fontID, "", fullPath.Get()));
1573}
1574
1575PlatformFontPtr IGraphicsWeb::LoadPlatformFont(const char* fontID, const char* fontName, ETextStyle style)
1576{
1577 const char* styles[] = { "normal", "bold", "italic" };
1578
1579 return PlatformFontPtr(new Font(fontName, styles[static_cast<int>(style)]));
1580}
1581
1582PlatformFontPtr IGraphicsWeb::LoadPlatformFont(const char* fontID, void* pData, int dataSize)
1583{
1584 return PlatformFontPtr(new MemoryFont(fontID, "", pData, dataSize));
1585}
1586
1587// Shadow DOM event callback implementations (called from JavaScript)
1588extern "C" {
1589
1590EMSCRIPTEN_KEEPALIVE
1591void iGraphicsMouseCallback(void* pGraphics, int eventType, double x, double y, double dx, double dy, int buttons, int button, int shift, int ctrl, int alt)
1592{
1593 IGraphicsWeb* pG = static_cast<IGraphicsWeb*>(pGraphics);
1594 float scale = pG->GetDrawScale();
1595
1596 IMouseInfo info;
1597 info.x = x / scale;
1598 info.y = y / scale;
1599 info.dX = dx;
1600 info.dY = dy;
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};
1603
1604 switch (eventType)
1605 {
1606 case 0: // mousedown
1607 pG->OnMouseDown(list);
1608 break;
1609 case 1: // mouseup
1610 list[0].ms.L = button == 0;
1611 list[0].ms.R = button == 2;
1612 pG->OnMouseUp(list);
1613 break;
1614 case 2: // mousemove
1615 if (buttons == 0)
1616 pG->OnMouseOver(info.x, info.y, info.ms);
1617 else if (!pG->IsInPlatformTextEntry())
1618 pG->OnMouseDrag(list);
1619 break;
1620 case 3: // mouseenter
1621 pG->OnSetCursor();
1622 pG->OnMouseOver(info.x, info.y, info.ms);
1623 break;
1624 case 4: // mouseleave
1625 pG->OnMouseOut();
1626 break;
1627 }
1628
1629 pG->mPrevX = info.x;
1630 pG->mPrevY = info.y;
1631}
1632
1633EMSCRIPTEN_KEEPALIVE
1634void iGraphicsWheelCallback(void* pGraphics, double x, double y, double deltaY, int shift, int ctrl, int alt)
1635{
1636 IGraphicsWeb* pG = static_cast<IGraphicsWeb*>(pGraphics);
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);
1640}
1641
1642#if !defined(NDEBUG) || defined(IPLUG_LIVE_EDIT)
1643EMSCRIPTEN_KEEPALIVE
1644int iplug_set_live_edit(void* pGraphics, int enabled)
1645{
1646 if (!pGraphics)
1647 return 0;
1648
1649 IGraphicsWeb* pG = static_cast<IGraphicsWeb*>(pGraphics);
1650 pG->EnableLiveEdit(enabled != 0);
1651 return pG->LiveEditEnabled() == (enabled != 0);
1652}
1653#endif
1654
1655} // extern "C"
1656
1657#if defined IGRAPHICS_NANOVG
1658#include "IGraphicsNanoVG.cpp"
1659
1660#ifdef IGRAPHICS_FREETYPE
1661#define FONS_USE_FREETYPE
1662#endif
1663
1664#include "nanovg.c"
1665#endif
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.
Definition: IGraphics.h:86
virtual ECursor SetMouseCursor(ECursor cursorType=ECursor::ARROW)
Sets the mouse cursor to one of ECursor (implementations should return the result of the base impleme...
Definition: IGraphics.h:828
void OnMouseDrag(const std::vector< IMouseInfo > &points)
Called when the platform class sends drag events.
Definition: IGraphics.cpp:1170
void OnMouseUp(const std::vector< IMouseInfo > &points)
Called when the platform class sends mouse up events.
Definition: IGraphics.cpp:1072
void OnTouchCancelled(const std::vector< IMouseInfo > &points)
Called when the platform class sends touch cancel events.
Definition: IGraphics.cpp:1113
void OnMouseDown(const std::vector< IMouseInfo > &points)
Called when the platform class sends mouse down events.
Definition: IGraphics.cpp:986
bool OnMouseWheel(float x, float y, const IMouseMod &mod, float delta)
Definition: IGraphics.cpp:1238
float GetDrawScale() const
Gets the graphics context scaling factor.
Definition: IGraphics.h:1118
IGraphics platform class for the web.
Definition: IGraphicsWeb.h:40
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.
Definition: IGraphicsWeb.h:56
IPlug's parameter class.
EParamType Type() const
Get the parameter's type.
A class to specify an item of a pop up menu.
A class for setting the contents of a pop up menu.
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...
Definition: IPlugStructs.h:615
Used to manage mouse modifiers i.e.
Used to manage a rectangular area, independent of draw class/platform.
float W() const
float H() const
IText is used to manage font and text/text entry style for a piece of text on the UI,...