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
19// Helper to create WebGL context for Shadow DOM (CSS selectors don't work)
20EM_JS(int, createWebGLContextForShadowDOM, (), {
21 var canvas = Module.canvas;
22 if (!canvas) return 0;
23 var attrs = { stencil: true, depth: true, antialias: true, alpha: true };
24 var ctx = canvas.getContext("webgl", attrs) || canvas.getContext("experimental-webgl", attrs);
25 if (!ctx) return 0;
26 return GL.registerContext(ctx, attrs);
27});
28
29
30BEGIN_IPLUG_NAMESPACE
31BEGIN_IGRAPHICS_NAMESPACE
32
33void GetScreenDimensions(int& width, int& height)
34{
35 width = val::global("window")["innerWidth"].as<int>();
36 height = val::global("window")["innerHeight"].as<int>();
37}
38
39END_IPLUG_NAMESPACE
40END_IGRAPHICS_NAMESPACE
41
42using namespace iplug;
43using namespace igraphics;
44using namespace emscripten;
45
46extern std::vector<IGraphicsWeb*> gGraphicsInstances;
47extern void UnregisterGraphicsInstance(IGraphicsWeb* pGraphics);
48double gPrevMouseDownTime = 0.;
49bool gFirstClick = false;
50
51#pragma mark - Private Classes and Structs
52
53// Fonts
54
55class IGraphicsWeb::Font : public PlatformFont
56{
57public:
58 Font(const char* fontName, const char* fontStyle)
59 : PlatformFont(true), mDescriptor{fontName, fontStyle}
60 {}
61
62 FontDescriptor GetDescriptor() override { return &mDescriptor; }
63
64private:
65 std::pair<WDL_String, WDL_String> mDescriptor;
66};
67
68class IGraphicsWeb::FileFont : public Font
69{
70public:
71 FileFont(const char* fontName, const char* fontStyle, const char* fontPath)
72 : Font(fontName, fontStyle), mPath(fontPath)
73 {
74 mSystem = false;
75 }
76
77 IFontDataPtr GetFontData() override;
78
79private:
80 WDL_String mPath;
81};
82
83IFontDataPtr IGraphicsWeb::FileFont::GetFontData()
84{
85 IFontDataPtr fontData(new IFontData());
86 FILE* fp = fopen(mPath.Get(), "rb");
87
88 // Read in the font data.
89 if (!fp)
90 return fontData;
91
92 fseek(fp,0,SEEK_END);
93 fontData = std::make_unique<IFontData>((int) ftell(fp));
94
95 if (!fontData->GetSize())
96 return fontData;
97
98 fseek(fp,0,SEEK_SET);
99 size_t readSize = fread(fontData->Get(), 1, fontData->GetSize(), fp);
100 fclose(fp);
101
102 if (readSize && readSize == fontData->GetSize())
103 fontData->SetFaceIdx(0);
104
105 return fontData;
106}
107
108class IGraphicsWeb::MemoryFont : public Font
109{
110public:
111 MemoryFont(const char* fontName, const char* fontStyle, const void* pData, int dataSize)
112 : Font(fontName, fontStyle)
113 {
114 mSystem = false;
115 mData.Set((const uint8_t*)pData, dataSize);
116 }
117
118 IFontDataPtr GetFontData() override
119 {
120 return IFontDataPtr(new IFontData(mData.Get(), mData.GetSize(), 0));
121 }
122
123private:
124 WDL_TypedBuf<uint8_t> mData;
125};
126
127#pragma mark - Utilities and Callbacks
128
129static EM_BOOL key_callback(int eventType, const EmscriptenKeyboardEvent* pEvent, void* pUserData)
130{
131 IGraphicsWeb* pGraphicsWeb = (IGraphicsWeb*) pUserData;
132
133 int VK = DOMKeyToVirtualKey(pEvent->keyCode);
134 WDL_String keyUTF8;
135
136 // filter utf8 for non ascii keys
137 if ((VK >= kVK_0 && VK <= kVK_Z) || VK == kVK_NONE)
138 keyUTF8.Set(pEvent->key);
139 else
140 keyUTF8.Set("");
141
142 IKeyPress keyPress {keyUTF8.Get(),
143 DOMKeyToVirtualKey(pEvent->keyCode),
144 static_cast<bool>(pEvent->shiftKey),
145 static_cast<bool>(pEvent->ctrlKey || pEvent->metaKey),
146 static_cast<bool>(pEvent->altKey)};
147
148 switch (eventType)
149 {
150 case EMSCRIPTEN_EVENT_KEYDOWN:
151 {
152 return pGraphicsWeb->OnKeyDown(pGraphicsWeb->mPrevX, pGraphicsWeb->mPrevY, keyPress);
153 }
154 case EMSCRIPTEN_EVENT_KEYUP:
155 {
156 return pGraphicsWeb->OnKeyUp(pGraphicsWeb->mPrevX, pGraphicsWeb->mPrevY, keyPress);
157 }
158 default:
159 break;
160 }
161
162 return 0;
163}
164
165static EM_BOOL outside_mouse_callback(int eventType, const EmscriptenMouseEvent* pEvent, void* pUserData)
166{
167 IGraphicsWeb* pGraphics = (IGraphicsWeb*) pUserData;
168
169 IMouseInfo info;
170 val rect = pGraphics->GetCanvas().call<val>("getBoundingClientRect");
171 info.x = (pEvent->targetX - rect["left"].as<double>()) / pGraphics->GetDrawScale();
172 info.y = (pEvent->targetY - rect["top"].as<double>()) / pGraphics->GetDrawScale();
173 info.dX = pEvent->movementX;
174 info.dY = pEvent->movementY;
175 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)};
176 std::vector<IMouseInfo> list {info};
177
178 switch (eventType)
179 {
180 case EMSCRIPTEN_EVENT_MOUSEUP:
181 {
182 // Get button states based on what caused the mouse up (nothing in buttons)
183 list[0].ms.L = pEvent->button == 0;
184 list[0].ms.R = pEvent->button == 2;
185 pGraphics->OnMouseUp(list);
186 emscripten_set_mousemove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, pGraphics, 1, nullptr);
187 emscripten_set_mouseup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, pGraphics, 1, nullptr);
188 break;
189 }
190 case EMSCRIPTEN_EVENT_MOUSEMOVE:
191 {
192 if(pEvent->buttons != 0 && !pGraphics->IsInPlatformTextEntry())
193 pGraphics->OnMouseDrag(list);
194 break;
195 }
196 default:
197 break;
198 }
199
200 pGraphics->mPrevX = info.x;
201 pGraphics->mPrevY = info.y;
202
203 return true;
204}
205
206static EM_BOOL mouse_callback(int eventType, const EmscriptenMouseEvent* pEvent, void* pUserData)
207{
208 IGraphicsWeb* pGraphics = (IGraphicsWeb*) pUserData;
209
210 IMouseInfo info;
211 info.x = pEvent->targetX / pGraphics->GetDrawScale();
212 info.y = pEvent->targetY / pGraphics->GetDrawScale();
213 info.dX = pEvent->movementX;
214 info.dY = pEvent->movementY;
215 info.ms = {(pEvent->buttons & 1) != 0,
216 (pEvent->buttons & 2) != 0,
217 static_cast<bool>(pEvent->shiftKey),
218 static_cast<bool>(pEvent->ctrlKey),
219 static_cast<bool>(pEvent->altKey)};
220
221 std::vector<IMouseInfo> list {info};
222 switch (eventType)
223 {
224 case EMSCRIPTEN_EVENT_MOUSEDOWN:
225 {
226 const double timestamp = GetTimestamp();
227 const double timeDiff = timestamp - gPrevMouseDownTime;
228
229 if (gFirstClick && timeDiff < 0.3)
230 {
231 gFirstClick = false;
232 pGraphics->OnMouseDblClick(info.x, info.y, info.ms);
233 }
234 else
235 {
236 gFirstClick = true;
237 pGraphics->OnMouseDown(list);
238 }
239
240 gPrevMouseDownTime = timestamp;
241
242 break;
243 }
244 case EMSCRIPTEN_EVENT_MOUSEUP:
245 {
246 // Get button states based on what caused the mouse up (nothing in buttons)
247 list[0].ms.L = pEvent->button == 0;
248 list[0].ms.R = pEvent->button == 2;
249 pGraphics->OnMouseUp(list);
250 break;
251 }
252 case EMSCRIPTEN_EVENT_MOUSEMOVE:
253 {
254 gFirstClick = false;
255
256 if(pEvent->buttons == 0)
257 pGraphics->OnMouseOver(info.x, info.y, info.ms);
258 else
259 {
260 if(!pGraphics->IsInPlatformTextEntry())
261 pGraphics->OnMouseDrag(list);
262 }
263 break;
264 }
265 case EMSCRIPTEN_EVENT_MOUSEENTER:
266 pGraphics->OnSetCursor();
267 pGraphics->OnMouseOver(info.x, info.y, info.ms);
268 emscripten_set_mousemove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, pGraphics, 1, nullptr);
269 break;
270 case EMSCRIPTEN_EVENT_MOUSELEAVE:
271 if(pEvent->buttons != 0)
272 {
273 emscripten_set_mousemove_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, pGraphics, 1, outside_mouse_callback);
274 emscripten_set_mouseup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, pGraphics, 1, outside_mouse_callback);
275 }
276 pGraphics->OnMouseOut(); break;
277 default:
278 break;
279 }
280
281 pGraphics->mPrevX = info.x;
282 pGraphics->mPrevY = info.y;
283
284 return true;
285}
286
287static EM_BOOL wheel_callback(int eventType, const EmscriptenWheelEvent* pEvent, void* pUserData)
288{
289 IGraphics* pGraphics = (IGraphics*) pUserData;
290
291 IMouseMod modifiers(false, false, pEvent->mouse.shiftKey, pEvent->mouse.ctrlKey, pEvent->mouse.altKey);
292
293 double x = pEvent->mouse.targetX;
294 double y = pEvent->mouse.targetY;
295
296 x /= pGraphics->GetDrawScale();
297 y /= pGraphics->GetDrawScale();
298
299 switch (eventType) {
300 case EMSCRIPTEN_EVENT_WHEEL: pGraphics->OnMouseWheel(x, y, modifiers, pEvent->deltaY);
301 default:
302 break;
303 }
304
305 return true;
306}
307
308EM_BOOL touch_callback(int eventType, const EmscriptenTouchEvent* pEvent, void* pUserData)
309{
310 IGraphics* pGraphics = (IGraphics*) pUserData;
311 const float drawScale = pGraphics->GetDrawScale();
312
313 std::vector<IMouseInfo> points;
314
315 static EmscriptenTouchPoint previousTouches[32];
316
317 for (auto i = 0; i < pEvent->numTouches; i++)
318 {
319 IMouseInfo info;
320 info.x = pEvent->touches[i].targetX / drawScale;
321 info.y = pEvent->touches[i].targetY / drawScale;
322 info.dX = info.x - (previousTouches[i].targetX / drawScale);
323 info.dY = info.y - (previousTouches[i].targetY / drawScale);
324 info.ms = {true,
325 false,
326 static_cast<bool>(pEvent->shiftKey),
327 static_cast<bool>(pEvent->ctrlKey),
328 static_cast<bool>(pEvent->altKey),
329 static_cast<ITouchID>(pEvent->touches[i].identifier)
330 };
331
332 if(pEvent->touches[i].isChanged)
333 points.push_back(info);
334 }
335
336 memcpy(previousTouches, pEvent->touches, sizeof(previousTouches));
337
338 switch (eventType)
339 {
340 case EMSCRIPTEN_EVENT_TOUCHSTART:
341 pGraphics->OnMouseDown(points);
342 return true;
343 case EMSCRIPTEN_EVENT_TOUCHEND:
344 pGraphics->OnMouseUp(points);
345 return true;
346 case EMSCRIPTEN_EVENT_TOUCHMOVE:
347 pGraphics->OnMouseDrag(points);
348 return true;
349 case EMSCRIPTEN_EVENT_TOUCHCANCEL:
350 pGraphics->OnTouchCancelled(points);
351 return true;
352 default:
353 return false;
354 }
355}
356
357static EM_BOOL complete_text_entry(int eventType, const EmscriptenFocusEvent* focusEvent, void* pUserData)
358{
359 IGraphicsWeb* pGraphics = (IGraphicsWeb*) pUserData;
360
361 val input = val::global("document").call<val>("getElementById", std::string("textEntry"));
362 std::string str = input["value"].as<std::string>();
363 val::global("document")["body"].call<void>("removeChild", input);
364 pGraphics->SetControlValueAfterTextEdit(str.c_str());
365
366 return true;
367}
368
369static EM_BOOL text_entry_keydown(int eventType, const EmscriptenKeyboardEvent* pEvent, void* pUserData)
370{
371 IGraphicsWeb* pGraphicsWeb = (IGraphicsWeb*) pUserData;
372
373 IKeyPress keyPress {pEvent->key, DOMKeyToVirtualKey(pEvent->keyCode),
374 static_cast<bool>(pEvent->shiftKey),
375 static_cast<bool>(pEvent->ctrlKey),
376 static_cast<bool>(pEvent->altKey)};
377
378 if (keyPress.VK == kVK_RETURN || keyPress.VK == kVK_TAB)
379 return complete_text_entry(0, nullptr, pUserData);
380
381 return false;
382}
383
384static EM_BOOL uievent_callback(int eventType, const EmscriptenUiEvent* pEvent, void* pUserData)
385{
386 IGraphicsWeb* pGraphics = (IGraphicsWeb*) pUserData;
387
388 if (eventType == EMSCRIPTEN_EVENT_RESIZE)
389 {
390 pGraphics->GetDelegate()->OnParentWindowResize(pEvent->windowInnerWidth, pEvent->windowInnerHeight);
391
392 return true;
393 }
394
395 return false;
396}
397
398IColorPickerHandlerFunc gColorPickerHandlerFunc = nullptr;
399
400static void color_picker_callback(val e)
401{
402 if(gColorPickerHandlerFunc)
403 {
404 std::string colorStrHex = e["target"]["value"].as<std::string>();
405
406 if (colorStrHex[0] == '#')
407 colorStrHex = colorStrHex.erase(0, 1);
408
409 IColor result;
410 result.A = 255;
411 sscanf(colorStrHex.c_str(), "%02x%02x%02x", &result.R, &result.G, &result.B);
412
413 gColorPickerHandlerFunc(result);
414 }
415}
416
417static void file_dialog_callback(val e)
418{
419 // DBGMSG(e["files"].as<std::string>().c_str());
420}
421
422EMSCRIPTEN_BINDINGS(events) {
423 function("color_picker_callback", color_picker_callback);
424 function("file_dialog_callback", file_dialog_callback);
425}
426
427#pragma mark -
428
429IGraphicsWeb::IGraphicsWeb(IGEditorDelegate& dlg, int w, int h, int fps, float scale, val canvas)
430: IGRAPHICS_DRAW_CLASS(dlg, w, h, fps, scale)
431{
432 val keys = val::global("Object").call<val>("keys", GetPreloadedImages());
433
434 DBGMSG("Preloaded %i images\n", keys["length"].as<int>());
435
436 // Initialize canvas - use provided element, Module.canvas, or fall back to getElementById
437 if (canvas.isUndefined() || canvas.isNull())
438 {
439 // Try Module.canvas first (set by web component or HTML template)
440 val moduleCanvas = val::global("Module")["canvas"];
441 if (!moduleCanvas.isUndefined() && !moduleCanvas.isNull())
442 {
443 mCanvas = moduleCanvas;
444 }
445 else
446 {
447 // Fall back to getElementById for legacy templates
448 mCanvas = val::global("document").call<val>("getElementById", std::string("canvas"));
449 }
450 }
451 else
452 {
453 mCanvas = canvas;
454 }
455
456 // Detect Shadow DOM by checking the canvas's root node
457 mRootNode = mCanvas.call<val>("getRootNode");
458 std::string rootNodeType = mRootNode["constructor"]["name"].as<std::string>();
459 mInShadowDOM = (rootNodeType == "ShadowRoot");
460
461 // Use the canvas's existing id if it has one (legacy templates rely on
462 // querying their <canvas id="canvas"> from JS); otherwise synthesize a
463 // per-instance id so multiple instances on a page don't collide.
464 std::string existingId = mCanvas["id"].as<std::string>();
465 if (existingId.empty())
466 {
467 char idBuf[64];
468 snprintf(idBuf, sizeof(idBuf), "iplug-canvas-%p", static_cast<void*>(this));
469 existingId = idBuf;
470 mCanvas.set("id", existingId);
471 }
472 mCanvasSelector = std::string("#") + existingId;
473
474 DBGMSG("IGraphicsWeb: Shadow DOM = %s, selector = %s\n", mInShadowDOM ? "true" : "false", mCanvasSelector.c_str());
475
476 RegisterCanvasEvents();
477}
478
479void IGraphicsWeb::RegisterCanvasEvents()
480{
481 // Window-level events always work
482 emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, 1, key_callback);
483 emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, 1, key_callback);
484 emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, 1, uievent_callback);
485
486 if (mInShadowDOM)
487 {
488 // Shadow DOM: emscripten's CSS selector-based callbacks don't work
489 // Set up events via JavaScript on the canvas element directly
490 EM_ASM({
491 var pGraphics = $0;
492 var canvas = Module.canvas;
493 if (!canvas) return;
494
495 // Store reference for cleanup
496 canvas._iplugGraphics = pGraphics;
497
498 canvas.addEventListener('mousedown', function(e) {
499 Module._iGraphicsMouseCallback(pGraphics, 0, e.offsetX, e.offsetY, e.movementX, e.movementY, e.buttons, e.button, e.shiftKey, e.ctrlKey, e.altKey);
500 });
501 canvas.addEventListener('mouseup', function(e) {
502 Module._iGraphicsMouseCallback(pGraphics, 1, e.offsetX, e.offsetY, e.movementX, e.movementY, e.buttons, e.button, e.shiftKey, e.ctrlKey, e.altKey);
503 });
504 canvas.addEventListener('mousemove', function(e) {
505 Module._iGraphicsMouseCallback(pGraphics, 2, e.offsetX, e.offsetY, e.movementX, e.movementY, e.buttons, e.button, e.shiftKey, e.ctrlKey, e.altKey);
506 });
507 canvas.addEventListener('mouseenter', function(e) {
508 Module._iGraphicsMouseCallback(pGraphics, 3, e.offsetX, e.offsetY, e.movementX, e.movementY, e.buttons, e.button, e.shiftKey, e.ctrlKey, e.altKey);
509 });
510 canvas.addEventListener('mouseleave', function(e) {
511 Module._iGraphicsMouseCallback(pGraphics, 4, e.offsetX, e.offsetY, e.movementX, e.movementY, e.buttons, e.button, e.shiftKey, e.ctrlKey, e.altKey);
512 });
513 canvas.addEventListener('wheel', function(e) {
514 Module._iGraphicsWheelCallback(pGraphics, e.offsetX, e.offsetY, e.deltaY, e.shiftKey, e.ctrlKey, e.altKey);
515 e.preventDefault();
516 }, { passive: false });
517 // TODO: Touch events for Shadow DOM
518 }, this);
519 }
520 else
521 {
522 // Regular DOM: use emscripten's callback system
523 const char* target = mCanvasSelector.c_str();
524 emscripten_set_mousedown_callback(target, this, 1, mouse_callback);
525 emscripten_set_mouseup_callback(target, this, 1, mouse_callback);
526 emscripten_set_mousemove_callback(target, this, 1, mouse_callback);
527 emscripten_set_mouseenter_callback(target, this, 1, mouse_callback);
528 emscripten_set_mouseleave_callback(target, this, 1, mouse_callback);
529 emscripten_set_wheel_callback(target, this, 1, wheel_callback);
530 emscripten_set_touchstart_callback(target, this, 1, touch_callback);
531 emscripten_set_touchend_callback(target, this, 1, touch_callback);
532 emscripten_set_touchmove_callback(target, this, 1, touch_callback);
533 emscripten_set_touchcancel_callback(target, this, 1, touch_callback);
534 }
535}
536
537void IGraphicsWeb::UnregisterCanvasEvents()
538{
539 const char* target = mCanvasSelector.c_str();
540
541 emscripten_set_mousedown_callback(target, this, 1, nullptr);
542 emscripten_set_mouseup_callback(target, this, 1, nullptr);
543 emscripten_set_mousemove_callback(target, this, 1, nullptr);
544 emscripten_set_mouseenter_callback(target, this, 1, nullptr);
545 emscripten_set_mouseleave_callback(target, this, 1, nullptr);
546 emscripten_set_wheel_callback(target, this, 1, nullptr);
547 emscripten_set_keydown_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, 1, nullptr);
548 emscripten_set_keyup_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, 1, nullptr);
549 emscripten_set_touchstart_callback(target, this, 1, nullptr);
550 emscripten_set_touchend_callback(target, this, 1, nullptr);
551 emscripten_set_touchmove_callback(target, this, 1, nullptr);
552 emscripten_set_touchcancel_callback(target, this, 1, nullptr);
553}
554
555IGraphicsWeb::~IGraphicsWeb()
556{
557 UnregisterCanvasEvents();
558 UnregisterGraphicsInstance(this);
559}
560
561void* IGraphicsWeb::OpenWindow(void* pHandle)
562{
563#ifdef IGRAPHICS_GL
564 EmscriptenWebGLContextAttributes attr;
565 emscripten_webgl_init_context_attributes(&attr);
566 attr.stencil = true;
567 attr.depth = true;
568// attr.explicitSwapControl = 1;
569
570 EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx;
571
572 if (mInShadowDOM)
573 {
574 // Shadow DOM: create context via JS since CSS selectors don't work
575 ctx = createWebGLContextForShadowDOM();
576 }
577 else
578 {
579 // Regular DOM: use standard emscripten API
580 ctx = emscripten_webgl_create_context(mCanvasSelector.c_str(), &attr);
581 }
582
583 emscripten_webgl_make_context_current(ctx);
584#endif
585
586 OnViewInitialized(nullptr /* not used */);
587
588 SetScreenScale(std::ceil(std::max(emscripten_get_device_pixel_ratio(), 1.)));
589
590 GetDelegate()->LayoutUI(this);
591 GetDelegate()->OnUIOpen();
592
593 return nullptr;
594}
595
596void IGraphicsWeb::HideMouseCursor(bool hide, bool lock)
597{
598 if (mCursorHidden == hide)
599 return;
600
601 if (hide)
602 {
603#ifdef IGRAPHICS_WEB_POINTERLOCK
604 if (lock)
605 emscripten_request_pointerlock(mCanvasSelector.c_str(), EM_FALSE);
606 else
607#endif
608 mCanvas["style"].set("cursor", "none");
609
610 mCursorHidden = true;
611 mCursorLock = lock;
612 }
613 else
614 {
615#ifdef IGRAPHICS_WEB_POINTERLOCK
616 if (mCursorLock)
617 emscripten_exit_pointerlock();
618 else
619#endif
620 OnSetCursor();
621
622 mCursorHidden = false;
623 mCursorLock = false;
624 }
625}
626
627ECursor IGraphicsWeb::SetMouseCursor(ECursor cursorType)
628{
629 std::string cursor("pointer");
630
631 switch (cursorType)
632 {
633 case ECursor::ARROW: cursor = "default"; break;
634 case ECursor::IBEAM: cursor = "text"; break;
635 case ECursor::WAIT: cursor = "wait"; break;
636 case ECursor::CROSS: cursor = "crosshair"; break;
637 case ECursor::UPARROW: cursor = "n-resize"; break;
638 case ECursor::SIZENWSE: cursor = "nwse-resize"; break;
639 case ECursor::SIZENESW: cursor = "nesw-resize"; break;
640 case ECursor::SIZEWE: cursor = "ew-resize"; break;
641 case ECursor::SIZENS: cursor = "ns-resize"; break;
642 case ECursor::SIZEALL: cursor = "move"; break;
643 case ECursor::INO: cursor = "not-allowed"; break;
644 case ECursor::HAND: cursor = "pointer"; break;
645 case ECursor::APPSTARTING: cursor = "progress"; break;
646 case ECursor::HELP: cursor = "help"; break;
647 }
648
649 mCanvas["style"].set("cursor", cursor);
650 return IGraphics::SetMouseCursor(cursorType);
651}
652
653void IGraphicsWeb::GetMouseLocation(float& x, float&y) const
654{
655 x = mPrevX;
656 y = mPrevY;
657}
658
659//static
660void IGraphicsWeb::OnMainLoopTimer()
661{
662 int screenScale = (int) std::ceil(std::max(emscripten_get_device_pixel_ratio(), 1.));
663
664 // Iterate over all registered graphics instances
665 for (IGraphicsWeb* pGraphics : gGraphicsInstances)
666 {
667 if (pGraphics == nullptr)
668 continue;
669
670 if (screenScale != pGraphics->GetScreenScale())
671 {
672 pGraphics->SetScreenScale(screenScale);
673 }
674
675 IRECTList rects;
676 if (pGraphics->IsDirty(rects))
677 {
678 pGraphics->SetAllControlsClean();
679 pGraphics->Draw(rects);
680 }
681 }
682}
683
684EMsgBoxResult IGraphicsWeb::ShowMessageBox(const char* str, const char* /*title*/, EMsgBoxType type, IMsgBoxCompletionHandlerFunc completionHandler)
685{
686 ReleaseMouseCapture();
687
688 EMsgBoxResult result = kNoResult;
689
690 switch (type)
691 {
692 case kMB_OK:
693 {
694 val::global("window").call<val>("alert", std::string(str));
695 result = EMsgBoxResult::kOK;
696 break;
697 }
698 case kMB_YESNO:
699 case kMB_OKCANCEL:
700 {
701 result = static_cast<EMsgBoxResult>(val::global("window").call<val>("confirm", std::string(str)).as<int>());
702 }
703 // case MB_CANCEL:
704 // break;
705 default:
706 return result = kNoResult;
707 }
708
709 if(completionHandler)
710 completionHandler(result);
711
712 return result;
713}
714
715void IGraphicsWeb::PromptForFile(WDL_String& filename, WDL_String& path, EFileAction action, const char* ext, IFileDialogCompletionHandlerFunc completionHandler)
716{
717 //TODO
718 // val inputEl = val::global("document").call<val>("createElement", std::string("input"));
719
720 // inputEl.call<void>("setAttribute", std::string("type"), std::string("file"));
721 // inputEl.call<void>("setAttribute", std::string("accept"), std::string(ext));
722 // inputEl.call<void>("click");
723 // inputEl.call<void>("addEventListener", std::string("input"), val::module_property("file_dialog_callback"), false);
724 // inputEl.call<void>("addEventListener", std::string("onChange"), val::module_property("file_dialog_callback"), false);
725}
726
727void IGraphicsWeb::PromptForDirectory(WDL_String& path, IFileDialogCompletionHandlerFunc completionHandler)
728{
729 //TODO
730 // val inputEl = val::global("document").call<val>("createElement", std::string("input"));
731
732 // inputEl.call<void>("setAttribute", std::string("type"), std::string("file"));
733 // inputEl.call<void>("setAttribute", std::string("directory"), true);
734 // inputEl.call<void>("setAttribute", std::string("webkitdirectory"), true);
735 // inputEl.call<void>("click");
736 // inputEl.call<void>("addEventListener", std::string("input"), val::module_property("file_dialog_callback"), false);
737 // inputEl.call<void>("addEventListener", std::string("onChange"), val::module_property("file_dialog_callback"), false);
738}
739
740bool IGraphicsWeb::PromptForColor(IColor& color, const char* str, IColorPickerHandlerFunc func)
741{
742 ReleaseMouseCapture();
743
744 gColorPickerHandlerFunc = func;
745
746 val inputEl = val::global("document").call<val>("createElement", std::string("input"));
747 inputEl.call<void>("setAttribute", std::string("type"), std::string("color"));
748 WDL_String colorStr;
749 colorStr.SetFormatted(64, "#%02x%02x%02x", color.R, color.G, color.B);
750 inputEl.call<void>("setAttribute", std::string("value"), std::string(colorStr.Get()));
751 inputEl.call<void>("click");
752 inputEl.call<void>("addEventListener", std::string("input"), val::module_property("color_picker_callback"), false);
753 inputEl.call<void>("addEventListener", std::string("onChange"), val::module_property("color_picker_callback"), false);
754
755 return false;
756}
757
758void IGraphicsWeb::CreatePlatformTextEntry(int paramIdx, const IText& text, const IRECT& bounds, int length, const char* str)
759{
760 val input = val::global("document").call<val>("createElement", std::string("input"));
761 val rect = mCanvas.call<val>("getBoundingClientRect");
762
763 auto setDim = [&input](const char *dimName, double pixels)
764 {
765 WDL_String dimstr;
766 dimstr.SetFormatted(32, "%fpx", pixels);
767 input["style"].set(dimName, std::string(dimstr.Get()));
768 };
769
770 auto setColor = [&input](const char *colorName, IColor color)
771 {
772 WDL_String str;
773 str.SetFormatted(64, "rgba(%d, %d, %d, %d)", color.R, color.G, color.B, color.A);
774 input["style"].set(colorName, std::string(str.Get()));
775 };
776
777 input.set("id", std::string("textEntry"));
778 input["style"].set("position", val("fixed"));
779 setDim("left", rect["left"].as<double>() + bounds.L);
780 setDim("top", rect["top"].as<double>() + bounds.T);
781 setDim("width", bounds.W());
782 setDim("height", bounds.H());
783
784 setColor("color", text.mTextEntryFGColor);
785 setColor("background-color", text.mTextEntryBGColor);
786 if (paramIdx > kNoParameter)
787 {
788 const IParam* pParam = GetDelegate()->GetParam(paramIdx);
789
790 switch (pParam->Type())
791 {
792 case IParam::kTypeEnum:
793 case IParam::kTypeInt:
794 case IParam::kTypeBool:
795 input.set("type", val("number")); // TODO
796 break;
797 case IParam::kTypeDouble:
798 input.set("type", val("number"));
799 break;
800 default:
801 break;
802 }
803 }
804 else
805 {
806 input.set("type", val("text"));
807 }
808
809 // Append to shadow root or document.body based on mode
810 if (mInShadowDOM)
811 {
812 mRootNode.call<void>("appendChild", input);
813 }
814 else
815 {
816 val::global("document")["body"].call<void>("appendChild", input);
817 }
818
819 input.call<void>("focus");
820 emscripten_set_focusout_callback("textEntry", this, 1, complete_text_entry);
821 emscripten_set_keydown_callback("textEntry", this, 1, text_entry_keydown);
822}
823
824IPopupMenu* IGraphicsWeb::CreatePlatformPopupMenu(IPopupMenu& menu, const IRECT bounds, bool& isAsync)
825{
826 return nullptr;
827}
828
829bool IGraphicsWeb::OpenURL(const char* url, const char* msgWindowTitle, const char* confirmMsg, const char* errMsgOnFailure)
830{
831 val::global("window").call<val>("open", std::string(url), std::string("_blank"));
832
833 return true;
834}
835
836void IGraphicsWeb::DrawResize()
837{
838 // CSS style.width/height need "px" suffix
839 std::string widthPx = std::to_string(static_cast<int>(Width() * GetDrawScale())) + "px";
840 std::string heightPx = std::to_string(static_cast<int>(Height() * GetDrawScale())) + "px";
841 mCanvas["style"].set("width", val(widthPx));
842 mCanvas["style"].set("height", val(heightPx));
843
844 // Canvas element width/height attributes are integers (no px).
845 // Assigning these clears the WebGL drawing buffer even when the value
846 // is unchanged, so guard against redundant writes — otherwise every
847 // ResizeObserver tick during a drag causes a visible flash.
848 const int newBufW = Width() * GetBackingPixelScale();
849 const int newBufH = Height() * GetBackingPixelScale();
850 const int curBufW = mCanvas["width"].as<int>();
851 const int curBufH = mCanvas["height"].as<int>();
852 if (newBufW != curBufW || newBufH != curBufH)
853 {
854 mCanvas.set("width", newBufW);
855 mCanvas.set("height", newBufH);
856 }
857
858 IGRAPHICS_DRAW_CLASS::DrawResize();
859}
860
861void IGraphicsWeb::PostResize()
862{
863 // Called at the end of IGraphics::Resize(), after OnResize +
864 // SetAllControlsDirty + DrawResize + (optional) LayoutUI have all
865 // run. At this point control layout is final, so it's safe to
866 // repaint synchronously. Without this, the canvas is stuck in a
867 // cleared state (canvas.width/height assignment wiped the default
868 // framebuffer; NanoVG's DrawResize rebuilt an empty FBO) until the
869 // next main-loop RAF tick, which the browser may composite past —
870 // producing a one-frame blank flash on every size change.
871 IRECTList rects;
872 rects.Add(GetBounds());
873 SetAllControlsClean();
874 Draw(rects);
875}
876
877PlatformFontPtr IGraphicsWeb::LoadPlatformFont(const char* fontID, const char* fileNameOrResID)
878{
879 WDL_String fullPath;
880 const EResourceLocation fontLocation = LocateResource(fileNameOrResID, "ttf", fullPath, GetBundleID(), nullptr, nullptr);
881
882 if (fontLocation == kNotFound)
883 return nullptr;
884
885 return PlatformFontPtr(new FileFont(fontID, "", fullPath.Get()));
886}
887
888PlatformFontPtr IGraphicsWeb::LoadPlatformFont(const char* fontID, const char* fontName, ETextStyle style)
889{
890 const char* styles[] = { "normal", "bold", "italic" };
891
892 return PlatformFontPtr(new Font(fontName, styles[static_cast<int>(style)]));
893}
894
895PlatformFontPtr IGraphicsWeb::LoadPlatformFont(const char* fontID, void* pData, int dataSize)
896{
897 return PlatformFontPtr(new MemoryFont(fontID, "", pData, dataSize));
898}
899
900// Shadow DOM event callback implementations (called from JavaScript)
901extern "C" {
902
903EMSCRIPTEN_KEEPALIVE
904void iGraphicsMouseCallback(void* pGraphics, int eventType, double x, double y, double dx, double dy, int buttons, int button, int shift, int ctrl, int alt)
905{
906 IGraphicsWeb* pG = static_cast<IGraphicsWeb*>(pGraphics);
907 float scale = pG->GetDrawScale();
908
909 IMouseInfo info;
910 info.x = x / scale;
911 info.y = y / scale;
912 info.dX = dx;
913 info.dY = dy;
914 info.ms = {(buttons & 1) != 0, (buttons & 2) != 0, static_cast<bool>(shift), static_cast<bool>(ctrl), static_cast<bool>(alt)};
915 std::vector<IMouseInfo> list{info};
916
917 switch (eventType)
918 {
919 case 0: // mousedown
920 pG->OnMouseDown(list);
921 break;
922 case 1: // mouseup
923 list[0].ms.L = button == 0;
924 list[0].ms.R = button == 2;
925 pG->OnMouseUp(list);
926 break;
927 case 2: // mousemove
928 if (buttons == 0)
929 pG->OnMouseOver(info.x, info.y, info.ms);
930 else if (!pG->IsInPlatformTextEntry())
931 pG->OnMouseDrag(list);
932 break;
933 case 3: // mouseenter
934 pG->OnSetCursor();
935 pG->OnMouseOver(info.x, info.y, info.ms);
936 break;
937 case 4: // mouseleave
938 pG->OnMouseOut();
939 break;
940 }
941
942 pG->mPrevX = info.x;
943 pG->mPrevY = info.y;
944}
945
946EMSCRIPTEN_KEEPALIVE
947void iGraphicsWheelCallback(void* pGraphics, double x, double y, double deltaY, int shift, int ctrl, int alt)
948{
949 IGraphicsWeb* pG = static_cast<IGraphicsWeb*>(pGraphics);
950 float scale = pG->GetDrawScale();
951 IMouseMod mod(false, false, static_cast<bool>(shift), static_cast<bool>(ctrl), static_cast<bool>(alt));
952 pG->OnMouseWheel(x / scale, y / scale, mod, deltaY);
953}
954
955} // extern "C"
956
957#if defined IGRAPHICS_NANOVG
958#include "IGraphicsNanoVG.cpp"
959
960#ifdef IGRAPHICS_FREETYPE
961#define FONS_USE_FREETYPE
962#endif
963
964#include "nanovg.c"
965#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 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,...