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