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 std.process,
23        std..string,
24        std.conv;
25 
26 import gfm.math.vector;
27 import gfm.math.box;
28 
29 import dplug.core.runtime;
30 import dplug.core.nogc;
31 
32 import dplug.graphics.image;
33 import dplug.graphics.view;
34 
35 import dplug.window.window;
36 
37 nothrow:
38 @nogc:
39 
40 
41 version(Windows)
42 {
43     import std.uuid;
44     import dplug.core.random;
45 
46     import core.sys.windows.windef;
47     import core.sys.windows.winuser;
48     import core.sys.windows.winbase;
49     import core.sys.windows.wingdi;
50 
51 
52     HINSTANCE getModuleHandle() nothrow @nogc
53     {
54         return GetModuleHandleA(null);
55     }
56 
57     final class Win32Window : IWindow
58     {
59     public:
60     nothrow:
61     @nogc:
62 
63         this(HWND parentWindow, IWindowListener listener, int width, int height)
64         {
65             _wndClass.style = CS_DBLCLKS | CS_OWNDC;
66 
67             _wndClass.lpfnWndProc = &windowProcCallback;
68 
69             _wndClass.cbClsExtra = 0;
70             _wndClass.cbWndExtra = 0;
71             _wndClass.hInstance = getModuleHandle();
72             _wndClass.hIcon = null;
73             _wndClass.hCursor = LoadCursor(null, IDC_ARROW);
74             _wndClass.hbrBackground = null;
75             _wndClass.lpszMenuName = null;
76 
77             // Generates an unique class name
78             generateClassName();
79             _wndClass.lpszClassName = _className.ptr;
80 
81             if (!RegisterClassW(&_wndClass))
82             {
83                 assert(false, "Couldn't register Win32 class");
84             }
85 
86             DWORD flags = WS_VISIBLE;
87             if (parentWindow != null)
88                 flags |= WS_CHILD;
89             else
90                 parentWindow = GetDesktopWindow();
91 
92             _hwnd = CreateWindowW(_className.ptr, null, flags, CW_USEDEFAULT, CW_USEDEFAULT, width, height,
93                                  parentWindow, null,
94                                  getModuleHandle(),
95                                  cast(void*)this);
96 
97             if (_hwnd is null)
98             {
99                 assert(false, "Couldn't create a Win32 window");
100             }
101 
102             _listener = listener;
103             // Sets this as user data
104             SetWindowLongPtrA(_hwnd, GWLP_USERDATA, cast(LONG_PTR)( cast(void*)this ));
105 
106             if (_listener !is null) // we are interested in custom behaviour
107             {
108 
109                 int mSec = 15; // refresh at 60 hz if possible
110                 SetTimer(_hwnd, TIMER_ID, mSec, null);
111             }
112 
113             SetFocus(_hwnd);
114 
115             // Get performance counter frequency
116             LARGE_INTEGER performanceFrequency;
117             BOOL res = QueryPerformanceFrequency(&performanceFrequency);
118             assert(res != 0); // since XP it is always supported
119             _performanceCounterDivider = performanceFrequency.QuadPart;
120 
121             // Get reference time
122             _timeAtCreationInMs = getTimeMs();
123             _lastMeasturedTimeInMs = _timeAtCreationInMs;
124         }
125 
126         ~this()
127         {
128             if (_hwnd != null)
129             {
130                 DestroyWindow(_hwnd);
131                 _hwnd = null;
132 
133                 // Unregister the window class, which was unique
134                 UnregisterClassW(_wndClass.lpszClassName, getModuleHandle());
135             }
136         }
137 
138         /// Returns: true if window size changed.
139         bool updateSizeIfNeeded()
140         {
141             RECT winsize;
142             BOOL res = GetClientRect(_hwnd, &winsize);
143             if (res == 0)
144             {
145                 assert(false, "GetClientRect failed");
146             }
147 
148             int newWidth = winsize.right - winsize.left;
149             int newHeight = winsize.bottom - winsize.top;
150 
151             // only do something if the client size has changed
152             if (newWidth != _width || newHeight != _height)
153             {
154                 _width = newWidth;
155                 _height = newHeight;
156 
157                 _wfb = _listener.onResized(_width, _height);
158                 return true;
159             }
160             else
161                 return false;
162         }
163 
164         LRESULT windowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
165         {
166             // because DispatchMessage is called by host, we don't know which thread comes here
167             ScopedForeignCallback!(true, true) scopedCallback;
168             scopedCallback.enter();
169 
170             if (_listener is null)
171                 return DefWindowProc(hwnd, uMsg, wParam, lParam);
172 
173             switch (uMsg)
174             {
175                 case WM_KEYDOWN:
176                 case WM_KEYUP:
177                 {
178                     bool handled = false;
179 
180                     shiftPressed = GetKeyState(VK_SHIFT) < 0;
181 
182                     Key key = vkToKey(wParam, shiftPressed);
183                     if (uMsg == WM_KEYDOWN)
184                     {
185                         if (_listener.onKeyDown(key))
186                         {
187                             sendRepaintIfUIDirty(); // do not wait for the timer
188                             handled = true;
189                         }
190                     }
191                     else
192                     {
193                         if (_listener.onKeyUp(key))
194                         {
195                             sendRepaintIfUIDirty(); // do not wait for the timer
196                             handled = true;
197                         }
198                     }
199 
200                     if (!handled)
201                     {
202                         // key is passed to the parent window
203                         HWND rootHWnd = GetAncestor(hwnd, GA_ROOT);
204                         SendMessage(rootHWnd, uMsg, wParam, lParam);
205                         return DefWindowProc(hwnd, uMsg, wParam, lParam);
206                     }
207                     else
208                         return 0;
209                 }
210 
211                 case WM_MOUSEMOVE:
212                     {
213                         int newMouseX = ( cast(int)lParam ) & 0xffff;
214                         int newMouseY = ( cast(int)lParam ) >> 16;
215                         int dx = newMouseX - _mouseX;
216                         int dy = newMouseY - _mouseY;
217                         _listener.onMouseMove(newMouseX, newMouseY, dx, dy, getMouseState(wParam));
218                         _mouseX = newMouseX;
219                         _mouseY = newMouseY;
220                         sendRepaintIfUIDirty();
221                         return 0;
222                     }
223 
224                 case WM_RBUTTONDOWN:
225                 case WM_RBUTTONDBLCLK:
226                 {
227                     if (mouseClick(_mouseX, _mouseY, MouseButton.right, uMsg == WM_RBUTTONDBLCLK, wParam))
228                         return 0; // handled
229                     goto default;
230                 }
231 
232                 case WM_LBUTTONDOWN:
233                 case WM_LBUTTONDBLCLK:
234                 {
235                     if (mouseClick(_mouseX, _mouseY, MouseButton.left, uMsg == WM_LBUTTONDBLCLK, wParam))
236                         return 0; // handled
237                     goto default;
238                 }
239 
240                 case WM_MBUTTONDOWN:
241                 case WM_MBUTTONDBLCLK:
242                 {
243                     if (mouseClick(_mouseX, _mouseY, MouseButton.middle, uMsg == WM_MBUTTONDBLCLK, wParam))
244                         return 0; // handled
245                     goto default;
246                 }
247 
248                 // X1/X2 buttons
249                 case WM_XBUTTONDOWN:
250                 case WM_XBUTTONDBLCLK:
251                 {
252                     auto mb = (wParam >> 16) == 1 ? MouseButton.x1 : MouseButton.x2;
253                     if (mouseClick(_mouseX, _mouseY, mb, uMsg == WM_XBUTTONDBLCLK, wParam))
254                         return 0;
255                     goto default;
256                 }
257 
258                 case WM_RBUTTONUP:
259                     if (mouseRelease(_mouseX, _mouseY, MouseButton.right, wParam))
260                         return 0;
261                     goto default;
262 
263                 case WM_LBUTTONUP:
264                     if (mouseRelease(_mouseX, _mouseY, MouseButton.left, wParam))
265                         return 0;
266                     goto default;
267                 case WM_MBUTTONUP:
268                     if (mouseRelease(_mouseX, _mouseY, MouseButton.middle, wParam))
269                         return 0;
270                     goto default;
271 
272                 case WM_XBUTTONUP:
273                 {
274                     auto mb = (wParam >> 16) == 1 ? MouseButton.x1 : MouseButton.x2;
275                     if (mouseRelease(_mouseX, _mouseY, mb, wParam))
276                         return 0;
277                     goto default;
278                 }
279 
280                 case WM_CAPTURECHANGED:
281                     _listener.onMouseCaptureCancelled();
282                     goto default;
283 
284                 case WM_PAINT:
285                 {
286                     RECT r;
287                     if (GetUpdateRect(hwnd, &r, FALSE))
288                     {
289                         bool sizeChanged = updateSizeIfNeeded();
290 
291                         // FUTURE: check resize work
292 
293                         // For efficiency purpose, render in BGRA for Windows
294                         _listener.onDraw(WindowPixelFormat.BGRA8);
295 
296                         box2i areaToRedraw = box2i(r.left, r.top, r.right, r.bottom);
297 
298                         box2i[] areasToRedraw = (&areaToRedraw)[0..1];
299                         swapBuffers(_wfb, areasToRedraw);
300                     }
301                     return 0;
302                 }
303 
304                 case WM_CLOSE:
305                 {
306                     this.destroyNoGC();
307                     return 0;
308                 }
309 
310                 case WM_TIMER:
311                 {
312                     if (wParam == TIMER_ID)
313                     {
314                         uint now = getTimeMs();
315                         double dt = (now - _lastMeasturedTimeInMs) * 0.001;
316                         double time = (now - _timeAtCreationInMs) * 0.001; // hopefully no plug-in will be open more than 49 days
317                         _lastMeasturedTimeInMs = now;
318                         _listener.onAnimate(dt, time);
319                         sendRepaintIfUIDirty();
320                     }
321                     return 0;
322                 }
323 
324                 case WM_SIZE:
325                 {
326                     _width = LOWORD(lParam);
327                     _height = HIWORD(lParam);
328                     return DefWindowProcA(hwnd, uMsg, wParam, lParam);
329                 }
330 
331                 default:
332                     return DefWindowProcA(hwnd, uMsg, wParam, lParam);
333             }
334         }
335 
336         void swapBuffers(ImageRef!RGBA wfb, box2i[] areasToRedraw)
337         {
338             PAINTSTRUCT paintStruct;
339             HDC hdc = BeginPaint(_hwnd, &paintStruct);
340 
341             foreach(box2i area; areasToRedraw)
342             {
343                 if (area.width() <= 0 || area.height() <= 0)
344                     continue; // nothing to update
345 
346                 BITMAPINFOHEADER bmi = BITMAPINFOHEADER.init; // fill with zeroes
347                 with (bmi)
348                 {
349                     biSize          = BITMAPINFOHEADER.sizeof;
350                     biWidth         = wfb.w;
351                     biHeight        = -wfb.h;
352                     biPlanes        = 1;
353                     biCompression = BI_RGB;
354                     biXPelsPerMeter = 72;
355                     biYPelsPerMeter = 72;
356                     biBitCount      = 32;
357                     biSizeImage     = cast(int)(wfb.pitch) * wfb.h;
358                     SetDIBitsToDevice(hdc, area.min.x, area.min.y, area.width, area.height,
359                                       area.min.x, -area.min.y - area.height + wfb.h, 0, wfb.h, wfb.pixels, cast(BITMAPINFO *)&bmi, DIB_RGB_COLORS);
360                 }
361             }
362 
363             EndPaint(_hwnd, &paintStruct);
364         }
365 
366         // Implements IWindow
367         override void waitEventAndDispatch()
368         {
369             MSG msg;
370             int ret = GetMessageW(&msg, _hwnd, 0, 0); // no range filtering
371             if (ret == -1)
372                 assert(false, "Error while in GetMessage");
373             TranslateMessage(&msg);
374             DispatchMessageW(&msg);
375         }
376 
377         override bool terminated()
378         {
379             return _terminated;
380         }
381 
382         override uint getTimeMs()
383         {
384             LARGE_INTEGER perfCounter;
385             BOOL err = QueryPerformanceCounter(&perfCounter);
386             assert(err != 0); // always supported since XP
387             double time = (perfCounter.QuadPart * 1000 + (_performanceCounterDivider >> 1)) / cast(double)_performanceCounterDivider;
388             return cast(uint)(time);
389         }
390 
391         override void* systemHandle()
392         {
393             return cast(void*)( cast(size_t)_hwnd );
394         }
395 
396     private:
397         enum TIMER_ID = 144;
398 
399         HWND _hwnd;
400 
401         WNDCLASSW _wndClass;
402 
403         long _performanceCounterDivider;
404         uint _timeAtCreationInMs;
405         uint _lastMeasturedTimeInMs;
406 
407         IWindowListener _listener; // contract: _listener must only be used in the message callback
408 
409         ImageRef!RGBA _wfb; // framebuffer reference
410 
411         bool _terminated = false;
412         int _width = 0;
413         int _height = 0;
414 
415         int _mouseX = 0;
416         int _mouseY = 0;
417 
418         bool shiftPressed = false;
419 
420         /// Propagates mouse events.
421         /// Returns: true if event handled.
422         bool mouseClick(int mouseX, int mouseY, MouseButton mb, bool isDoubleClick, WPARAM wParam)
423         {
424             SetFocus(_hwnd);   // get keyboard focus
425             SetCapture(_hwnd); // start mouse capture
426             bool consumed = _listener.onMouseClick(mouseX, mouseY, mb, isDoubleClick, getMouseState(wParam));
427             if (consumed)
428                 sendRepaintIfUIDirty(); // do not wait for the timer
429             return consumed;
430         }
431 
432         /// ditto
433         bool mouseRelease(int mouseX, int mouseY, MouseButton mb, WPARAM wParam)
434         {
435             ReleaseCapture();
436             bool consumed = _listener.onMouseRelease(mouseX, mouseY, mb, getMouseState(wParam));
437             if (consumed)
438                 sendRepaintIfUIDirty(); // do not wait for the timer
439             return consumed;
440         }
441 
442         /// Provokes a WM_PAINT if some UI element is dirty.
443         /// FUTURE: this function should be as fast as possible, maybe invalidate differently?
444         void sendRepaintIfUIDirty()
445         {
446             _listener.recomputeDirtyAreas();
447             box2i dirtyRect = _listener.getDirtyRectangle();
448             if (!dirtyRect.empty())
449             {
450                 RECT r = RECT(dirtyRect.min.x, dirtyRect.min.y, dirtyRect.max.x, dirtyRect.max.y);
451                 // MAYDO: maybe use RedrawWindow instead
452                 InvalidateRect(_hwnd, &r, FALSE); // FUTURE: invalidate rects one by one
453                 UpdateWindow(_hwnd);
454             }
455         }
456 
457         wchar[43] _className; // Zero-terminated class name
458 
459         void generateClassName() nothrow @nogc
460         {
461             generateNullTerminatedRandomUUID!wchar(_className, "dplug_"w);
462         }
463     }
464 
465 
466     extern(Windows) nothrow
467     {
468         LRESULT windowProcCallback(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
469         {
470             Win32Window window = cast(Win32Window)( cast(void*)(GetWindowLongPtrA(hwnd, GWLP_USERDATA)) );
471             if (window !is null)
472                 return window.windowProc(hwnd, uMsg, wParam, lParam);
473             else
474                 return DefWindowProcA(hwnd, uMsg, wParam, lParam);
475         }
476     }
477 
478     Key vkToKey(WPARAM vk, bool shiftPressed)pure nothrow @nogc
479     {
480         switch (vk)
481             {
482                 case VK_SPACE: return Key.space;
483 
484                 case VK_UP: return Key.upArrow;
485                 case VK_DOWN: return Key.downArrow;
486                 case VK_LEFT: return Key.leftArrow;
487                 case VK_RIGHT: return Key.rightArrow;
488 
489                 case VK_NUMPAD0: return Key.digit0;
490                 case VK_NUMPAD1: return Key.digit1;
491                 case VK_NUMPAD2: return Key.digit2;
492                 case VK_NUMPAD3: return Key.digit3;
493                 case VK_NUMPAD4: return Key.digit4;
494                 case VK_NUMPAD5: return Key.digit5;
495                 case VK_NUMPAD6: return Key.digit6;
496                 case VK_NUMPAD7: return Key.digit7;
497                 case VK_NUMPAD8: return Key.digit8;
498                 case VK_NUMPAD9: return Key.digit9;
499                 case 0x30: return Key.digit0;
500                 case 0x31: return Key.digit1;
501                 case 0x32: return Key.digit2;
502                 case 0x33: return Key.digit3;
503                 case 0x34: return Key.digit4;
504                 case 0x35: return Key.digit5;
505                 case 0x36: return Key.digit6;
506                 case 0x37: return Key.digit7;
507                 case 0x38: return Key.digit8;
508                 case 0x39: return Key.digit9;
509                 case 0x41: return shiftPressed ?  Key.A : Key.a;
510                 case 0x42: return shiftPressed ?  Key.B : Key.b;
511                 case 0x43: return shiftPressed ?  Key.C : Key.c;
512                 case 0x44: return shiftPressed ?  Key.D : Key.d;
513                 case 0x45: return shiftPressed ?  Key.E : Key.e;
514                 case 0x46: return shiftPressed ?  Key.F : Key.f;
515                 case 0x47: return shiftPressed ?  Key.G : Key.g;
516                 case 0x48: return shiftPressed ?  Key.H : Key.h;
517                 case 0x49: return shiftPressed ?  Key.I : Key.i;
518                 case 0x4A: return shiftPressed ?  Key.J : Key.j;
519                 case 0x4B: return shiftPressed ?  Key.K : Key.k;
520                 case 0x4C: return shiftPressed ?  Key.L : Key.l;
521                 case 0x4D: return shiftPressed ?  Key.M : Key.m;
522                 case 0x4E: return shiftPressed ?  Key.N : Key.n;
523                 case 0x4F: return shiftPressed ?  Key.O : Key.o;
524                 case 0x50: return shiftPressed ?  Key.P : Key.p;
525                 case 0x51: return shiftPressed ?  Key.Q : Key.q;
526                 case 0x52: return shiftPressed ?  Key.R : Key.r;
527                 case 0x53: return shiftPressed ?  Key.S : Key.s;
528                 case 0x54: return shiftPressed ?  Key.T : Key.t;
529                 case 0x55: return shiftPressed ?  Key.U : Key.u;
530                 case 0x56: return shiftPressed ?  Key.V : Key.v;
531                 case 0x57: return shiftPressed ?  Key.W : Key.w;
532                 case 0x58: return shiftPressed ?  Key.X : Key.x;
533                 case 0x59: return shiftPressed ?  Key.Y : Key.y;
534                 case 0x5A: return shiftPressed ?  Key.Z : Key.z;
535                 case VK_BACK: return Key.backspace;
536                 case VK_RETURN: return Key.enter;
537                 case VK_ESCAPE: return Key.escape;
538                 default: return Key.unsupported;
539             }
540     }
541 
542     SHORT keyState(int vk)
543     {
544         version(AAX)
545             return GetAsyncKeyState(VK_MENU);
546         else
547             return GetKeyState(VK_MENU);
548     }
549 
550     static MouseState getMouseState(WPARAM wParam)
551     {
552         return MouseState( (wParam & MK_LBUTTON) != 0,
553                            (wParam & MK_RBUTTON) != 0,
554                            (wParam & MK_MBUTTON) != 0,
555                            (wParam & MK_XBUTTON1) != 0,
556                            (wParam & MK_XBUTTON2) != 0,
557                            (wParam & MK_CONTROL) != 0,
558                            (wParam & MK_SHIFT) != 0,
559                            keyState(VK_MENU) < 0 );
560     }
561 }