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 }