1 /*
2 Cockos WDL License
3 
4 Copyright (C) 2005 - 2015 Cockos Incorporated
5 Copyright (C) 2015 - 2016 Auburn Sounds
6 
7 Portions copyright other contributors, see each source file for more information
8 
9 This software is provided 'as-is', without any express or implied warranty.  In no event will the authors be held liable for any damages arising from the use of this software.
10 
11 Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
12 
13 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
14 1. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
15 1. This notice may not be removed or altered from any source distribution.
16 */
17 /**
18     Win32 window implementation.
19 */
20 module dplug.window.win32window;
21 
22 import dplug.math.vector;
23 import dplug.math.box;
24 
25 import dplug.core.runtime;
26 import dplug.core.nogc;
27 import dplug.core.vec;
28 
29 import dplug.graphics.image;
30 
31 import dplug.window.window;
32 
33 
34 version(legacyMouseCursor)
35 {
36     version = noCursors; // FUTURE: tell to replace with Dplug_NoMouseCursor
37 }
38 else version(Dplug_NoMouseCursor)
39 {
40     version = noCursors;
41 }
42 
43 
44 // Hook the mouse callback so that you can use mouse wheel while dragging outside of the plugin 
45 // window boundaries. This is most useful for eg. EQ plugins where you would want to use mouse 
46 // wheel while dragging a point out of the plugin window.
47 // This option is Windows-only, it is ignored on Linux and macOS, where such a thing always works.
48 // Dragging + mouse wheel inside the plugin window doesn't need that option.
49 // See: https://github.com/AuburnSounds/Dplug/wiki/More-Options
50 version(Dplug_MouseWheelOutsideWindow)
51 {
52     // Note: can be annoying while debugging, as it is a global mouse hook.
53     // But the only way to drag outside of window with mouse wheel working.
54     // It is amazing that Windows even allow that.
55     debug
56     {    
57     }
58     else
59     {
60         version = hookMouseForWheelEvents;
61     }
62 }
63 
64 nothrow:
65 @nogc:
66 
67 
68 // Disabled, remaining issues in #536
69 // - very few hosts actually give the needed DPI
70 // - resizing that window is in some cases a challenge
71 // - using "mixed" DPI support to force Hi-DPI looks like a bad idea
72 // - not always easy to resize the child window
73 // - need testing in Windows 8 or something where some calls are missing
74 enum DPISupportWin32 = false; 
75 
76 version(Windows)
77 {
78     import dplug.core.random;
79 
80     import core.sys.windows.windef;
81     import core.sys.windows.winuser;
82     import core.sys.windows.winbase;
83     import core.sys.windows.wingdi;
84 
85     HINSTANCE getModuleHandle() nothrow @nogc
86     {
87         return GetModuleHandleA(null);
88     }
89 
90     final class Win32Window : IWindow
91     {
92     public:
93     nothrow:
94     @nogc:
95 
96         this(HWND parentWindow, IWindowListener listener, int width, int height)
97         {
98             _wndClass.style = CS_DBLCLKS | CS_OWNDC | CS_VREDRAW | CS_HREDRAW;
99 
100             _wndClass.lpfnWndProc = &windowProcCallback;
101 
102             _wndClass.cbClsExtra = 0;
103             _wndClass.cbWndExtra = 0;
104             _wndClass.hInstance = getModuleHandle();
105             _wndClass.hIcon = null;
106             _wndClass.hCursor = LoadCursor(null, IDC_ARROW);
107             _wndClass.hbrBackground = null;
108             _wndClass.lpszMenuName = null;
109 
110             // Generates an unique class name
111             generateClassName();
112             _wndClass.lpszClassName = _className.ptr;
113 
114             if (!RegisterClassW(&_wndClass))
115             {
116                 assert(false, "Couldn't register Win32 class");
117             }
118 
119             DWORD flags = 0;
120             if (parentWindow != null)
121                 flags |= WS_CHILD;
122             else
123                 parentWindow = GetDesktopWindow();
124 
125             _hwnd = CreateWindowW(_className.ptr, null, flags, CW_USEDEFAULT, CW_USEDEFAULT, 
126                                   width, height,
127                                   parentWindow, null,
128                                   getModuleHandle(),
129                                   cast(void*)this);
130 
131             if (_hwnd is null)
132             {
133                 assert(false, "Couldn't create a Win32 window");
134             }
135 
136             // Create update region
137             _updateRegion = CreateRectRgn(0, 0, 0, 0);
138             _clipRegion = CreateRectRgn(0, 0, 0, 0);
139             _updateRgbBuf = makeVec!ubyte();
140             _updateRects = makeVec!box2i();
141 
142             _listener = listener;
143             // Sets this as user data
144             SetWindowLongPtrA(_hwnd, GWLP_USERDATA, cast(LONG_PTR)( cast(void*)this ));
145 
146             if (_listener !is null) // we are interested in custom behavior
147             {
148 
149                 int mSec = 15; // refresh at 60 hz if possible
150                 SetTimer(_hwnd, TIMER_ID, mSec, null);
151             }
152 
153             // Resize if DPI is supported.
154             if (DPISupportWin32)
155             {
156                 _dpiSupport.initialize();
157                 _currentDPI = _dpiSupport.GetDpiForWindow(_hwnd) / 96.0f;
158                 //MAYDO resize for DPI
159                 //resizeWindowForDPI();
160             }
161 
162             ShowWindow(_hwnd, SW_SHOW);
163             SetFocus(_hwnd);
164 
165             // Get performance counter frequency
166             LARGE_INTEGER performanceFrequency;
167             BOOL res = QueryPerformanceFrequency(&performanceFrequency);
168             assert(res != 0); // since XP it is always supported
169             _performanceCounterDivider = performanceFrequency.QuadPart;
170 
171             // Get reference time
172             _timeAtCreationInMs = getTimeMs();
173             _lastMeasturedTimeInMs = _timeAtCreationInMs;
174 
175             // Do we need the FLStudio bridge work-around?
176             // Detect if we are under FLStudio's bridge.
177             _useFLStudioBridgeWorkaround = false;
178             HMODULE hmodule = GetModuleHandle(NULL);
179             if (hmodule !is NULL)
180             {
181                 char[256] path;
182                 int len = GetModuleFileNameA(hmodule, path.ptr, 256);
183                 if (len >= 12)
184                 {
185                     _useFLStudioBridgeWorkaround = path[len - 12 .. len] == "ilbridge.exe";
186                 }
187             }
188             version(hookMouseForWheelEvents)
189                 installMouseHook();
190         }
191 
192         ~this()
193         {
194             version(hookMouseForWheelEvents)
195                 removeMouseHook();
196 
197             if (_hwnd != null)
198             {
199                 DestroyWindow(_hwnd);
200                 _hwnd = null;
201 
202                 // Unregister the window class, which was unique
203                 UnregisterClassW(_wndClass.lpszClassName, getModuleHandle());
204             }
205 
206             if (_updateRegion != null)
207             {
208                 DeleteObject(_updateRegion);
209                 _updateRegion = null;
210             }
211 
212             if (_clipRegion != null)
213             {
214                 DeleteObject(_clipRegion);
215                 _clipRegion = null;
216             }
217         }
218 
219         /// Returns: true if window size changed.
220         /// Update internal buffers, if needed.
221         bool updateInternalSize()
222         {
223             RECT winsize;
224             BOOL res = GetClientRect(_hwnd, &winsize);
225             if (res == 0)
226             {
227                 assert(false, "GetClientRect failed");
228             }
229 
230             int newWidth = winsize.right - winsize.left;
231             int newHeight = winsize.bottom - winsize.top;
232 
233             // only do something if the client size has changed
234             if (newWidth != _width || newHeight != _height)
235             {
236                 _width = newWidth;
237                 _height = newHeight;
238 
239                 _sizeSet = true;
240                 _wfb = _listener.onResized(_width, _height);
241                 return true;
242             }
243             else
244                 return false;
245         }
246 
247         override bool requestResize(int widthLogicalPixels, int heightLogicalPixels, bool alsoResizeParentWindow)
248         {
249             UINT flags =  SWP_NOACTIVATE 
250                         | SWP_NOZORDER
251                         | SWP_NOOWNERZORDER
252                         | SWP_NOMOVE
253                         | SWP_NOCOPYBITS; // discard entire content of the client area
254 
255             RECT formerClient;
256             GetClientRect(_hwnd, &formerClient);
257 
258             int dw = widthLogicalPixels - (formerClient.right - formerClient.left);
259             int dh = heightLogicalPixels - (formerClient.bottom - formerClient.top);
260 
261             HWND parent = null;
262             RECT parentClient;
263             HWND gparent = null;
264             RECT gparentClient;
265             if (alsoResizeParentWindow)
266             {
267                 if (IsChildWindow(_hwnd))
268                 {
269                     parent = GetParent(_hwnd);
270                     GetClientRect(parent, &parentClient);
271                     if (IsChildWindow(parent))
272                     {
273                         gparent = GetParent(parent);
274                         GetClientRect(gparent, &gparentClient);
275                     }
276                 }
277             }
278 
279             BOOL r = SetWindowPos(_hwnd, null,
280                                   0, 0, 0 + widthLogicalPixels, 0 + heightLogicalPixels,
281                                   flags);
282             bool result = r != 0;
283 
284             if (parent !is null)
285             {
286                 int parentWidth = (parentClient.right - parentClient.left) + dw;
287                 int parentHeight = (parentClient.bottom - parentClient.top) + dh;
288                 SetWindowPos(parent, null,
289                              0, 0, 0 + parentWidth, 0 + parentHeight,
290                              SWP_NOZORDER | SWP_NOMOVE | SWP_NOACTIVATE);
291             }
292             if (gparent !is null)
293             {
294                 int gparentWidth = (gparentClient.right - gparentClient.left) + dw;
295                 int gparentHeight = (gparentClient.bottom - gparentClient.top) + dh;
296                 SetWindowPos(gparent, null,
297                              0, 0, 0 + gparentWidth, 0 + gparentHeight,
298                              SWP_NOZORDER | SWP_NOMOVE | SWP_NOACTIVATE);
299             }
300             return result;
301         }
302 
303         LRESULT windowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
304         {
305             // because DispatchMessage is called by host, we don't know which thread comes here
306             ScopedForeignCallback!(true, true) scopedCallback;
307             scopedCallback.enter();
308 
309             if (_listener is null)
310                 return DefWindowProc(hwnd, uMsg, wParam, lParam);
311 
312             switch (uMsg)
313             {
314                 case WM_KEYDOWN:
315                 case WM_KEYUP:
316                 {
317                     bool handled = false;
318 
319                     shiftPressed = GetKeyState(VK_SHIFT) < 0;
320 
321                     Key key = vkToKey(wParam, shiftPressed);
322                     if (uMsg == WM_KEYDOWN)
323                     {
324                         if (_listener.onKeyDown(key))
325                         {
326                             handled = true;
327                         }
328                     }
329                     else
330                     {
331                         if (_listener.onKeyUp(key))
332                         {
333                             handled = true;
334                         }
335                     }
336 
337                     if (!handled)
338                     {
339                         // TODO: looks fishy, why GA_ROOT instead of GA_PARENT?
340                         // key is passed to the most-parent window
341                         HWND rootHWnd = GetAncestor(hwnd, GA_ROOT);
342                         if (rootHWnd != hwnd) // send message up the chain if not self (not hosting)
343                         {
344                             SendMessage(rootHWnd, uMsg, wParam, lParam);
345                             return DefWindowProc(hwnd, uMsg, wParam, lParam);
346                         }
347                         else
348                             return DefWindowProc(hwnd, uMsg, wParam, lParam);
349                     }
350                     else
351                         return 0;
352                 }
353 
354 
355                 case WM_SETCURSOR:
356                 {
357                     // In case the cursor was modified by another window, restore the cursor.
358                     // This is done differently depending on whether we want a modifed cursor ourselves.
359                     // Easily tested in REAPER by hovering over the left border of 
360                     // the plugin: this turn the custor into a resize arrow.                    
361                     version(noCursors)
362                     {
363                         goto default;
364                     }
365                     else
366                         return setMouseCursor(true);
367                 }
368 
369                 case WM_MOUSEMOVE:
370                     {
371                         // See Dplug #378, important to sign-extend for multiple monitors
372                         int newMouseX = cast(int)(lParam << 16) >> 16; 
373                         int newMouseY = ( cast(int)lParam ) >> 16;
374                         int dx = newMouseX - _mouseX;
375                         int dy = newMouseY - _mouseY;
376                         _listener.onMouseMove(newMouseX, newMouseY, dx, dy, getMouseState(wParam));
377                         _mouseX = newMouseX;
378                         _mouseY = newMouseY;
379                         setMouseCursor(false);
380 
381                         // Track when mouse exit the window
382                         if (!_mouseTracking)
383                         {
384                             // Enable mouse tracking.
385                             TRACKMOUSEEVENT tme;
386                             tme.cbSize = tme.sizeof;
387                             tme.hwndTrack = hwnd;
388                             tme.dwFlags = TME_LEAVE;
389                             tme.dwHoverTime = HOVER_DEFAULT;
390                             TrackMouseEvent(&tme);
391                             _mouseTracking = true;
392                         }
393 
394                         return 0;
395                     }
396 
397                 case WM_SETFOCUS:
398                 {
399                     return 0;
400                 }
401 
402                 case WM_KILLFOCUS:
403                 {
404                     return 0;
405                 }
406 
407                 case WM_MOUSEWHEEL:
408                     {
409                         // See Dplug #378, important to sign-extend for multiple monitors
410                         int mouseX = cast(int)(lParam << 16) >> 16; 
411                         int mouseY = ( cast(int)lParam ) >> 16;
412 
413                         // Mouse positions we are getting are not relative to the client area
414                         RECT r;
415                         GetWindowRect(hwnd, &r);
416                         mouseX -= r.left;
417                         mouseY -= r.top;
418 
419                         int wheelDeltaY = cast(short)((wParam & 0xffff0000) >> 16) / WHEEL_DELTA;      
420                         
421                         if (_listener.onMouseWheel(mouseX, mouseY, 0, wheelDeltaY, getMouseState(wParam)))
422                         {
423                             return 0; // handled
424                         }
425                         goto default;
426                     }
427 
428                 case WM_RBUTTONDOWN:
429                 case WM_RBUTTONDBLCLK:
430                 {
431                     if (mouseClick(_mouseX, _mouseY, MouseButton.right, uMsg == WM_RBUTTONDBLCLK, wParam))
432                         return 0; // handled
433                     goto default;
434                 }
435 
436                 case WM_LBUTTONDOWN:
437                 case WM_LBUTTONDBLCLK:
438                 {
439                     if (mouseClick(_mouseX, _mouseY, MouseButton.left, uMsg == WM_LBUTTONDBLCLK, wParam))
440                         return 0; // handled
441                     goto default;
442                 }
443 
444                 case WM_MBUTTONDOWN:
445                 case WM_MBUTTONDBLCLK:
446                 {
447                     if (mouseClick(_mouseX, _mouseY, MouseButton.middle, uMsg == WM_MBUTTONDBLCLK, wParam))
448                         return 0; // handled
449                     goto default;
450                 }
451 
452                 // X1/X2 buttons
453                 case WM_XBUTTONDOWN:
454                 case WM_XBUTTONDBLCLK:
455                 {
456                     auto mb = (wParam >> 16) == 1 ? MouseButton.x1 : MouseButton.x2;
457                     if (mouseClick(_mouseX, _mouseY, mb, uMsg == WM_XBUTTONDBLCLK, wParam))
458                         return 0;
459                     goto default;
460                 }
461 
462                 case WM_RBUTTONUP:
463                     if (mouseRelease(_mouseX, _mouseY, MouseButton.right, wParam))
464                         return 0;
465                     goto default;
466 
467                 case WM_LBUTTONUP:
468                     if (mouseRelease(_mouseX, _mouseY, MouseButton.left, wParam))
469                         return 0;
470                     goto default;
471                 case WM_MBUTTONUP:
472                     if (mouseRelease(_mouseX, _mouseY, MouseButton.middle, wParam))
473                         return 0;
474                     goto default;
475 
476                 case WM_XBUTTONUP:
477                 {
478                     auto mb = (wParam >> 16) == 1 ? MouseButton.x1 : MouseButton.x2;
479                     if (mouseRelease(_mouseX, _mouseY, mb, wParam))
480                         return 0;
481                     goto default;
482                 }
483 
484                 case WM_CAPTURECHANGED:
485                     _listener.onMouseCaptureCancelled();
486                     setMouseCursor(true);
487                     goto default;
488 
489                 case WM_MOUSELEAVE:
490                     _listener.onMouseExitedWindow();
491                     _mouseTracking = false;
492                     return 0;
493 
494                 case WM_SIZE:
495                 case WM_SHOWWINDOW:
496                 {
497                     // This get the size of the client area, and then tells the listener about the new size.
498                     updateInternalSize();
499                     goto default;
500                 }
501 
502                 case WM_PAINT:
503                 {
504                     // Internal size must have been set.
505                     assert(_sizeSet);
506 
507                     // Same thing that under Cocoa and Linux, the first WM_PAINT could happen before WM_TIMER is ever called.
508                     // In this case, call `recomputeDirtyAreas()` so that onDraw draw something and uninitialized pixels are not displayed.
509                     // See Issue #523 and #572.
510                     if (_dirtyAreasAreNotYetComputed)
511                     {
512                         _dirtyAreasAreNotYetComputed = false;
513                         _listener.recomputeDirtyAreas();
514                     }
515 
516                     // Renders UI. 
517                     // FUTURE: This could be done in a separate thread?
518                     // For efficiency purpose, render in BGRA for Windows
519                     // We do it here, but note that redrawing has nothing to do with WM_PAINT specifically,
520                     // we just need to wait for it here.
521                     _listener.onDraw(WindowPixelFormat.BGRA8);
522 
523                     // Get the update region
524                     int type = GetUpdateRgn(hwnd, _updateRegion, FALSE);
525                     assert (type != ERROR);
526 
527                     // Begin painting
528                     PAINTSTRUCT paintStruct;
529                     HDC hdc = BeginPaint(_hwnd, &paintStruct);
530 
531                     HRGN regionToUpdate = _updateRegion;
532 
533                     // FLStudio compatibility
534                     // Try to get the DC's clipping region, which may be larger in the case of FLStudio's bridge.
535                     if (_useFLStudioBridgeWorkaround)
536                     {
537                         if ( GetClipRgn(hdc, _clipRegion) == 1)
538                             regionToUpdate = _clipRegion;
539                     }
540 
541                     // Get needed number of bytes
542                     DWORD bytes = GetRegionData(regionToUpdate, 0, null);
543                     _updateRgbBuf.resize(bytes);
544 
545                     if (bytes == GetRegionData(regionToUpdate, bytes, cast(RGNDATA*)(_updateRgbBuf.ptr)))
546                     {
547                         // Get rectangles to update visually from the update region
548                         ubyte* buf = _updateRgbBuf.ptr;
549                         RGNDATAHEADER* header = cast(RGNDATAHEADER*)buf;
550                         assert(header.iType == RDH_RECTANGLES);
551                         _updateRects.clearContents();
552                         RECT* pRect = cast(RECT*)(buf + RGNDATAHEADER.sizeof);
553 
554                         alias wfb = _wfb;
555                         for (int r = 0; r < header.nCount; ++r)
556                         {
557                             int left = pRect[r].left;
558                             int top = pRect[r].top;
559                             int right = pRect[r].right;
560                             int bottom = pRect[r].bottom;
561                             _updateRects.pushBack(box2i(left, top, right, bottom));
562                         }
563 
564                         static int byteStride(int width) pure nothrow @nogc @safe
565                         {
566                             enum scanLineAlignment = 4;
567                             int widthInBytes = width * 4;
568                             return (widthInBytes + (scanLineAlignment - 1)) & ~(scanLineAlignment-1);
569                         }
570                         // Note: the pitch of the displayed image _has_ to follow `byteStride`.
571                         // SetDIBitsToDevice doesn't seem to take arbitrary pitch
572                         assert(wfb.pitch == byteStride(wfb.w));
573 
574                         BITMAPINFOHEADER bmi = BITMAPINFOHEADER.init; // fill with zeroes
575                         with (bmi)
576                         {
577                             biSize          = BITMAPINFOHEADER.sizeof;
578                             biWidth         = wfb.w;
579                             biHeight        = -wfb.h;
580                             biPlanes        = 1;
581                             biCompression = BI_RGB;
582                             biXPelsPerMeter = 72;
583                             biYPelsPerMeter = 72;
584                             biBitCount      = 32;
585                             biSizeImage     = cast(int)(wfb.pitch) * wfb.h;
586                         }
587                         
588                         foreach(box2i area; _updateRects)
589                         {
590                             if (area.width() <= 0 || area.height() <= 0)
591                                 continue; // nothing to update
592 
593                             SetDIBitsToDevice(hdc, area.min.x, area.min.y, area.width, area.height,
594                                                 area.min.x, -area.min.y - area.height + wfb.h, 
595                                                 0, wfb.h, wfb.pixels, cast(BITMAPINFO *)&bmi, DIB_RGB_COLORS);
596                         }
597                     }
598                     else
599                         assert(false);
600 
601                     EndPaint(_hwnd, &paintStruct);
602                     return 0;
603                 }
604 
605                 case WM_ERASEBKGND:
606                 {
607                     // This fails, so cause this window's WM_PAINT to be responsible for erasing background, 
608                     // hence saving a bit of performance.
609                     return 1;
610                 }
611 
612                 case WM_CLOSE:
613                 {
614                     this.destroyNoGC();
615                     return 0;
616                 }
617 
618                 case WM_TIMER:
619                 {
620                     if (wParam == TIMER_ID)
621                     {
622                         uint now = getTimeMs();
623                         double dt = (now - _lastMeasturedTimeInMs) * 0.001;
624                         double time = (now - _timeAtCreationInMs) * 0.001; // hopefully no plug-in will be open more than 49 days
625                         _lastMeasturedTimeInMs = now;
626                         _listener.onAnimate(dt, time);
627 
628                         _listener.recomputeDirtyAreas();
629                         _dirtyAreasAreNotYetComputed = false;
630                         box2i dirtyRect = _listener.getDirtyRectangle();
631                         if (!dirtyRect.empty())
632                         {
633                             RECT r = RECT(dirtyRect.min.x, dirtyRect.min.y, dirtyRect.max.x, dirtyRect.max.y);
634                             InvalidateRect(_hwnd, &r, FALSE); // FUTURE: invalidate rects one by one
635 
636                             // See issue #432 and #269
637                             // To avoid blocking WM_TIMER with expensive WM_PAINT, it's important NOT to enqueue manually a 
638                             // WM_PAINT here. Let Windows do its job of sending WM_PAINT when needed.
639                         }
640                     }
641                     return 0;
642                 }
643 
644                 default:
645                     return DefWindowProcA(hwnd, uMsg, wParam, lParam);
646             }
647         }
648 
649         // Implements IWindow
650         override void waitEventAndDispatch()
651         {
652             MSG msg;
653             int ret = GetMessageW(&msg, _hwnd, 0, 0); // no range filtering
654             if (ret == -1)
655                 assert(false, "Error while in GetMessage");
656             TranslateMessage(&msg);
657             DispatchMessageW(&msg);
658         }
659 
660         override bool terminated()
661         {
662             return _terminated;
663         }
664 
665         override uint getTimeMs()
666         {
667             LARGE_INTEGER perfCounter;
668             BOOL err = QueryPerformanceCounter(&perfCounter);
669             assert(err != 0); // always supported since XP
670             double time = (perfCounter.QuadPart * 1000 + (_performanceCounterDivider >> 1)) / cast(double)_performanceCounterDivider;
671             return cast(uint)(time);
672         }
673 
674         override void* systemHandle()
675         {
676             return cast(void*)( cast(size_t)_hwnd );
677         }
678 
679     private:
680         enum TIMER_ID = 144;
681 
682         HWND _hwnd;
683 
684         WNDCLASSW _wndClass;
685 
686         HRGN _updateRegion;
687         HRGN _clipRegion;
688         bool _useFLStudioBridgeWorkaround;
689 
690         Vec!ubyte _updateRgbBuf;
691         Vec!box2i _updateRects;
692 
693         long _performanceCounterDivider;
694         uint _timeAtCreationInMs;
695         uint _lastMeasturedTimeInMs;
696 
697         bool _sizeSet = false;
698         IWindowListener _listener; // contract: _listener must only be used in the message callback
699 
700         ImageRef!RGBA _wfb; // framebuffer reference
701 
702         bool _terminated = false;
703         int _width = 0;
704         int _height = 0;
705 
706         int _mouseX = 0;
707         int _mouseY = 0;
708         bool _mouseTracking = false;
709 
710         bool shiftPressed = false;
711         bool _dirtyAreasAreNotYetComputed = true;
712 
713         // Helper for getting DPI information.
714         DPIHelper _dpiSupport;
715 
716         // Current UI scale, 1.0f means default DPI (96).
717         // See_also: https://www.enlyze.com/blog/writing-win32-apps-like-its-2020/part-3/
718         float _currentDPI = 1.0f; 
719 
720         // Last MouseCursor used. This is to avoid updating the cursor
721         // more often than necessary
722         // Default value of pointer
723         MouseCursor _lastMouseCursor = MouseCursor.pointer;
724 
725         /// Propagates mouse events.
726         /// Returns: true if event handled.
727         bool mouseClick(int mouseX, int mouseY, MouseButton mb, bool isDoubleClick, WPARAM wParam)
728         {
729             SetFocus(_hwnd);   // get keyboard focus
730 
731             // Start mouse capture, if not already captures (See Issue #694)
732             if (GetCapture() != _hwnd)
733                 SetCapture(_hwnd); 
734 
735             bool consumed = _listener.onMouseClick(mouseX, mouseY, mb, isDoubleClick, getMouseState(wParam));
736             return consumed;
737         }
738 
739         /// ditto
740         bool mouseRelease(int mouseX, int mouseY, MouseButton mb, WPARAM wParam)
741         {
742             // Note: the first button release from mouse interrupts the dragging operation.
743             // This need not be the case, but it makes things easier.
744             // Not sure how cross platform we are on that specific point.
745             if (GetCapture() == _hwnd)
746                 ReleaseCapture();
747 
748             bool consumed = _listener.onMouseRelease(mouseX, mouseY, mb, getMouseState(wParam));
749             return consumed;
750         }
751 
752         wchar[43] _className; // Zero-terminated class name
753 
754         void generateClassName() nothrow @nogc
755         {
756             generateNullTerminatedRandomUUID!wchar(_className, "dplug_"w);
757         }
758 
759         int setMouseCursor(bool forceUpdate)
760         {
761             version(noCursors)
762             {}
763             else
764             {
765                 MouseCursor cursor = _listener.getMouseCursor();
766 
767                 if((cursor != _lastMouseCursor) || forceUpdate )
768                 {
769                     CURSORINFO pci;
770                     pci.cbSize = CURSORINFO.sizeof;
771                     GetCursorInfo(&pci);
772 
773                     // If the cursor we want to display is "hidden" and the cursor is being shown
774                     // then we will hide the cursor.
775                     // If the cursor we want to display is anything other than "hidden" and the
776                     // cursor is being hidden already, we will set it to show 
777                     // (this triggers a WM_SETCURSOR which will call this to set the cursor)
778                     // lastly if the above conditions are false then we will set the cursor
779                     if(cursor == MouseCursor.hidden && pci.flags == CURSOR_SHOWING)
780                     {
781                         ShowCursor(false);
782                     }
783                     else if(cursor != MouseCursor.hidden && pci.flags == 0)
784                     {
785                         ShowCursor(true);
786                     }
787                     else
788                     {
789                         auto cursorId = mouseCursorToCursorId(cursor);
790                         HCURSOR hc = LoadCursorA(NULL, cast(const(char)*)cursorId);
791                         SetCursor(hc);
792                     }
793                     _lastMouseCursor = cursor;
794                     return 1;
795                 }
796             }
797 
798             return 0;
799         }
800 
801         version(hookMouseForWheelEvents)
802         {
803 
804             // This prop says that this is a Dplug window, that has hooked the mouse.
805             // useful to identiy things when you just have a HWND.
806             // having does NOT mean that you can cast to Win32Window or anything, just that
807             // you expect the mouse wheel events to come to you in a drag outside your bounds.
808             enum string HOOK_PROPERTY = "i-am-a-dplug-window-and-have-hooked-the-mouse";
809 
810             // true if installed
811             bool installMouseHook()
812             {
813                 _mouseHook = SetWindowsHookExA(WH_MOUSE_LL, &LowLevelMouseProc, null, 0);
814 
815                 if (_mouseHook)
816                 {
817                     BOOL res = SetPropA(_hwnd, HOOK_PROPERTY.ptr, _mouseHook);
818                     if (res == 0)
819                     {
820                         removeMouseHook();
821                         return false;
822                     }
823                 }
824                 return (_mouseHook !is null);
825             }
826 
827             // true if removed
828             void removeMouseHook()
829             {
830                 // Remove property, if any
831                 if (null != GetPropA(_hwnd, HOOK_PROPERTY.ptr))
832                 {
833                     RemovePropA(_hwnd, HOOK_PROPERTY.ptr);
834                 }
835 
836                 // Note: nothing prevent the hook to be currently called there.
837                 if (_mouseHook) 
838                 {
839                     UnhookWindowsHookEx(_mouseHook);
840                     _mouseHook = null;
841                 }
842             }
843 
844             HHOOK _mouseHook = null;
845         }
846     }
847 
848 
849     extern(Windows) nothrow
850     {
851         LRESULT windowProcCallback(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
852         {
853             Win32Window window = cast(Win32Window)( cast(void*)(GetWindowLongPtrA(hwnd, GWLP_USERDATA)) );
854             if (window !is null)
855                 return window.windowProc(hwnd, uMsg, wParam, lParam);
856             else
857                 return DefWindowProcA(hwnd, uMsg, wParam, lParam);
858         }
859     }
860 
861     Key vkToKey(WPARAM vk, bool shiftPressed)pure nothrow @nogc
862     {
863         switch (vk)
864             {
865                 case VK_SPACE: return Key.space;
866 
867                 case VK_UP: return Key.upArrow;
868                 case VK_DOWN: return Key.downArrow;
869                 case VK_LEFT: return Key.leftArrow;
870                 case VK_RIGHT: return Key.rightArrow;
871 
872                 case VK_NUMPAD0: return Key.digit0;
873                 case VK_NUMPAD1: return Key.digit1;
874                 case VK_NUMPAD2: return Key.digit2;
875                 case VK_NUMPAD3: return Key.digit3;
876                 case VK_NUMPAD4: return Key.digit4;
877                 case VK_NUMPAD5: return Key.digit5;
878                 case VK_NUMPAD6: return Key.digit6;
879                 case VK_NUMPAD7: return Key.digit7;
880                 case VK_NUMPAD8: return Key.digit8;
881                 case VK_NUMPAD9: return Key.digit9;
882                 case 0x30: return Key.digit0;
883                 case 0x31: return Key.digit1;
884                 case 0x32: return Key.digit2;
885                 case 0x33: return Key.digit3;
886                 case 0x34: return Key.digit4;
887                 case 0x35: return Key.digit5;
888                 case 0x36: return Key.digit6;
889                 case 0x37: return Key.digit7;
890                 case 0x38: return Key.digit8;
891                 case 0x39: return Key.digit9;
892                 case 0x41: return shiftPressed ?  Key.A : Key.a;
893                 case 0x42: return shiftPressed ?  Key.B : Key.b;
894                 case 0x43: return shiftPressed ?  Key.C : Key.c;
895                 case 0x44: return shiftPressed ?  Key.D : Key.d;
896                 case 0x45: return shiftPressed ?  Key.E : Key.e;
897                 case 0x46: return shiftPressed ?  Key.F : Key.f;
898                 case 0x47: return shiftPressed ?  Key.G : Key.g;
899                 case 0x48: return shiftPressed ?  Key.H : Key.h;
900                 case 0x49: return shiftPressed ?  Key.I : Key.i;
901                 case 0x4A: return shiftPressed ?  Key.J : Key.j;
902                 case 0x4B: return shiftPressed ?  Key.K : Key.k;
903                 case 0x4C: return shiftPressed ?  Key.L : Key.l;
904                 case 0x4D: return shiftPressed ?  Key.M : Key.m;
905                 case 0x4E: return shiftPressed ?  Key.N : Key.n;
906                 case 0x4F: return shiftPressed ?  Key.O : Key.o;
907                 case 0x50: return shiftPressed ?  Key.P : Key.p;
908                 case 0x51: return shiftPressed ?  Key.Q : Key.q;
909                 case 0x52: return shiftPressed ?  Key.R : Key.r;
910                 case 0x53: return shiftPressed ?  Key.S : Key.s;
911                 case 0x54: return shiftPressed ?  Key.T : Key.t;
912                 case 0x55: return shiftPressed ?  Key.U : Key.u;
913                 case 0x56: return shiftPressed ?  Key.V : Key.v;
914                 case 0x57: return shiftPressed ?  Key.W : Key.w;
915                 case 0x58: return shiftPressed ?  Key.X : Key.x;
916                 case 0x59: return shiftPressed ?  Key.Y : Key.y;
917                 case 0x5A: return shiftPressed ?  Key.Z : Key.z;
918                 case VK_BACK: return Key.backspace;
919                 case VK_RETURN: return Key.enter;
920                 case VK_DELETE: return Key.suppr;
921                 case VK_ESCAPE: return Key.escape;
922                 default: return Key.unsupported;
923             }
924     }
925 
926     SHORT keyState(int vk)
927     {
928         version(AAX)
929             return GetAsyncKeyState(VK_MENU);
930         else
931             return GetKeyState(VK_MENU);
932     }
933 
934     static MouseState getMouseState(WPARAM wParam)
935     {
936         return MouseState( (wParam & MK_LBUTTON) != 0,
937                            (wParam & MK_RBUTTON) != 0,
938                            (wParam & MK_MBUTTON) != 0,
939                            (wParam & MK_XBUTTON1) != 0,
940                            (wParam & MK_XBUTTON2) != 0,
941                            (wParam & MK_CONTROL) != 0,
942                            (wParam & MK_SHIFT) != 0,
943                            keyState(VK_MENU) < 0 );
944     }
945 
946     HCURSOR mouseCursorToCursorId(MouseCursor cursor)
947     {
948         switch(cursor)
949         {
950             case cursor.linkSelect:
951                 return IDC_HAND;
952 
953             case cursor.drag:
954                 return IDC_CROSS;
955 
956             case cursor.move:
957                 return IDC_HAND;
958 
959             case cursor.horizontalResize:
960                 return IDC_SIZEWE;
961 
962             case cursor.verticalResize:
963                 return IDC_SIZENS;
964 
965             case cursor.diagonalResize:
966                 return IDC_SIZENWSE;
967 
968             case cursor.pointer:
969             
970             default:
971                 return IDC_ARROW;
972         }
973     }
974 
975 
976 // Copyright 2013 The Flutter Authors. All rights reserved.
977 // 
978 // Redistribution and use in source and binary forms, with or without modification,
979 //     are permitted provided that the following conditions are met:
980 // 
981 // * Redistributions of source code must retain the above copyright
982 // notice, this list of conditions and the following disclaimer.
983 // * Redistributions in binary form must reproduce the above
984 // copyright notice, this list of conditions and the following
985 // disclaimer in the documentation and/or other materials provided
986 // with the distribution.
987 // * Neither the name of Google Inc. nor the names of its
988 // contributors may be used to endorse or promote products derived
989 // from this software without specific prior written permission.
990 // 
991 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
992 // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
993 // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
994 // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
995 // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
996 // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
997 //  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
998 // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
999 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
1000 // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1001 
1002     /// A helper class for abstracting various Windows DPI related functions across
1003     /// Windows OS versions.
1004     private struct DPIHelper 
1005     {
1006     public:
1007     nothrow:
1008     @nogc:
1009         void initialize()
1010         {
1011             if ((user32_module_ = LoadLibraryA("User32.dll")) != null) 
1012             {
1013                 get_dpi_for_window_ = cast(GetDpiForWindow_t) GetProcAddress(user32_module_, "GetDpiForWindow".ptr);
1014                 dpi_for_window_supported_ = get_dpi_for_window_ !is null;
1015 
1016                 set_thread_dpi_awareness_context = cast(SetThreadDpiAwarenessContext_t) GetProcAddress(user32_module_, "SetThreadDpiAwarenessContext".ptr);
1017                 set_thread_dpi_hosting_behavior = cast(SetThreadDpiHostingBehavior_t) GetProcAddress(user32_module_, "SetThreadDpiHostingBehavior".ptr);
1018             }
1019             if ((shlib_module_ = LoadLibraryA("Shcore.dll")) != null) 
1020             {
1021                 get_dpi_for_monitor_ = cast(GetDpiForMonitor_t) GetProcAddress(shlib_module_, "GetDpiForMonitor".ptr);
1022                 dpi_for_monitor_supported_ = get_dpi_for_monitor_ !is null;
1023             }
1024         }
1025 
1026         ~this()
1027         {
1028             if (user32_module_ != null) 
1029             {
1030                 FreeLibrary(user32_module_);
1031                 user32_module_ = null;
1032             }
1033             if (shlib_module_ != null) 
1034             {
1035                 FreeLibrary(shlib_module_);
1036                 shlib_module_ = null;
1037             }
1038         }
1039 
1040     private:
1041         extern(Windows) nothrow @nogc
1042         {
1043             alias GetDpiForWindow_t = UINT function(HWND);
1044             alias GetDpiForMonitor_t = HRESULT function(HMONITOR hmonitor,
1045                                                         UINT dpiType,
1046                                                         UINT* dpiX,
1047                                                         UINT* dpiY);
1048             alias EnableNonClientDpiScaling_t = BOOL function(HWND);
1049 
1050             alias SetThreadDpiAwarenessContext_t = DPI_AWARENESS_CONTEXT function(DPI_AWARENESS_CONTEXT);
1051             alias SetThreadDpiHostingBehavior_t = DPI_HOSTING_BEHAVIOR function(DPI_HOSTING_BEHAVIOR);
1052         }
1053 
1054         GetDpiForWindow_t get_dpi_for_window_;
1055         GetDpiForMonitor_t get_dpi_for_monitor_;
1056 
1057         SetThreadDpiAwarenessContext_t set_thread_dpi_awareness_context;
1058         SetThreadDpiHostingBehavior_t set_thread_dpi_hosting_behavior;
1059 
1060         EnableNonClientDpiScaling_t enable_non_client_dpi_scaling_;
1061         HMODULE user32_module_ = null;
1062         HMODULE shlib_module_ = null;
1063         bool dpi_for_window_supported_ = false;
1064         bool dpi_for_monitor_supported_ = false;
1065 
1066 
1067         /// Returns the DPI for |hwnd|. Supports all DPI awareness modes, and is
1068         /// backward compatible down to Windows Vista. If |hwnd| is nullptr, returns
1069         /// the DPI for the primary monitor. If Per-Monitor DPI awareness is not
1070         /// available, returns the system's DPI.
1071         UINT GetDpiForWindow(HWND hwnd)
1072         {
1073             // GetDpiForWindow returns the DPI for any awareness mode. If not available,
1074             // or no |hwnd| is provided, fallback to a per monitor, system, or default
1075             // DPI.
1076             if (dpi_for_window_supported_ && hwnd != null) {
1077                 return get_dpi_for_window_(hwnd);
1078             }
1079 
1080             if (dpi_for_monitor_supported_) 
1081             {
1082                 HMONITOR monitor = null;
1083                 if (hwnd != null) {
1084                     monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY);
1085                 }
1086                 return GetDpiForMonitor(monitor);
1087             }
1088             HDC hdc = GetDC(hwnd);
1089             UINT dpi = GetDeviceCaps(hdc, LOGPIXELSX);
1090             ReleaseDC(hwnd, hdc);
1091             return dpi;
1092         }
1093 
1094         enum kDefaultDpi = 96;
1095 
1096         /// Returns the DPI of a given monitor. Defaults to 96 if the API is not
1097         /// available.
1098         UINT GetDpiForMonitor(HMONITOR monitor)
1099         {
1100             if (dpi_for_monitor_supported_) 
1101             {
1102                 if (monitor == null) 
1103                 {
1104                     POINT target_point;
1105                     target_point.x = 0;
1106                     target_point.y = 0;
1107                     monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTOPRIMARY);
1108                 }
1109                 UINT dpi_x = 0, 
1110                      dpi_y = 0;
1111 
1112                 HRESULT result = get_dpi_for_monitor_(monitor, 0 /* kEffectiveDpiMonitorType */, &dpi_x, &dpi_y);
1113                 if (SUCCEEDED(result)) 
1114                 {
1115                     return dpi_x;
1116                 }
1117             }
1118             return kDefaultDpi;
1119         }
1120 
1121         DPI_AWARENESS_CONTEXT SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT context)
1122         {
1123             // Do not set DPI to unsupported if asked so.
1124             if (context == DPI_AWARENESS_CONTEXT_UNSUPPORTED)
1125                 return context;
1126 
1127             if (set_thread_dpi_awareness_context !is null)
1128             {
1129                 return set_thread_dpi_awareness_context(context);
1130             }
1131             else
1132                 return DPI_AWARENESS_CONTEXT_UNSUPPORTED;
1133         }
1134 
1135         DPI_HOSTING_BEHAVIOR SetThreadDpiHostingBehavior(DPI_HOSTING_BEHAVIOR value)
1136         {
1137             if (value == DPI_HOSTING_UNSUPPORTED)
1138                 return value;
1139 
1140             if (set_thread_dpi_hosting_behavior !is null)
1141                 return set_thread_dpi_hosting_behavior(value);
1142             else
1143                 return value; // do nothing but pretend it went OK
1144         }
1145     }
1146 
1147     alias DPI_AWARENESS_CONTEXT = void*;
1148     enum : DPI_AWARENESS_CONTEXT
1149     {
1150         DPI_AWARENESS_CONTEXT_UNSUPPORTED          = cast(void*)0, // This API not supported
1151         DPI_AWARENESS_CONTEXT_UNAWARE              = cast(void*)(-1),
1152         DPI_AWARENESS_CONTEXT_SYSTEM_AWARE         = cast(void*)(-2),
1153         DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE    = cast(void*)(-3),
1154         DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = cast(void*)(-4),
1155         DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED    = cast(void*)(-5)  // since Windows 10 
1156     }
1157 
1158     alias DPI_HOSTING_BEHAVIOR = int;
1159     enum : DPI_HOSTING_BEHAVIOR
1160     {
1161         DPI_HOSTING_UNSUPPORTED      = -2,
1162         DPI_HOSTING_BEHAVIOR_INVALID = -1,
1163         DPI_HOSTING_BEHAVIOR_DEFAULT = 0,
1164         DPI_HOSTING_BEHAVIOR_MIXED = 1
1165     }
1166 
1167     static bool IsChildWindow(HWND pWnd)
1168     {
1169         if (pWnd)
1170         {
1171             int style = GetWindowLong(pWnd, GWL_STYLE);
1172             int exStyle = GetWindowLong(pWnd, GWL_EXSTYLE);
1173             return ((style & WS_CHILD) && !(exStyle & WS_EX_MDICHILD));
1174         }
1175         return false;
1176     }
1177 
1178 
1179     version(hookMouseForWheelEvents)
1180     {
1181         extern(Windows) LRESULT LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam) 
1182         {
1183             bool processed = false;
1184 
1185             if (nCode == 0) 
1186             {
1187                 if (wParam == WM_MOUSEWHEEL) // this is about a global mouse wheel event
1188                 {
1189                     // What window has the mouse capture, if any?
1190                     HWND hwnd = GetCapture();
1191 
1192                     if (hwnd != null)
1193                     {
1194                         // We need to make sure this is destined to a Dplug-window.
1195                         // It is one such window, one that can hook the mouse. Send it the mouse wheel event.
1196                         // Indeed, casting to Win32Window would not be completely safe.
1197                         // Actual mouse data is there.
1198                         if (null != GetPropA(hwnd, Win32Window.HOOK_PROPERTY.ptr))
1199                         {
1200                             MSLLHOOKSTRUCT* hookData = cast(MSLLHOOKSTRUCT*) lParam;
1201                             DWORD lParamMsg = cast(short)(hookData.pt.x) | (cast(short)(hookData.pt.y) << 16);
1202 
1203                             BOOL res = PostMessageA(hwnd, 
1204                                                     WM_MOUSEWHEEL, 
1205                                                     hookData.mouseData & 0xffff0000,  // Note: no support for horizontal mouse wheel
1206                                                     lParamMsg);
1207                             processed = (res != 0); // posted = processed
1208                         }
1209                     }
1210                 }
1211             }
1212 
1213             if (!processed)
1214                 return CallNextHookEx(null, nCode, wParam, lParam); // never consume the message
1215             else
1216             {
1217                 /// If the hook procedure processed the message, it may return a nonzero value to 
1218                 /// prevent the system from passing the message to the rest of the hook chain or 
1219                 /// the target window procedure.
1220                 return 1;
1221             }
1222         }
1223     }
1224 }