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 }