1 module steps2; 2 3 /+ 4 + A prototype step-sequencer Widget example. You can press and hold 5 + the left mouse button and drag the mouse to activate multiple steps. 6 + If you first clicked on an active step, the mode is changed to 7 + deactivating, so the steps you drag over will be deactivated. 8 + 9 + Extended version has a timer, time steps, and is heavily refactored. 10 +/ 11 12 import core.memory; 13 import core.runtime; 14 import core.thread; 15 import core.stdc.config; 16 17 import std.algorithm; 18 import std.array; 19 import std.conv; 20 import std.exception; 21 import std.functional; 22 import std.math; 23 import std.random; 24 import std.range; 25 import std.stdio; 26 import std.string; 27 import std.traits; 28 import std.utf; 29 30 pragma(lib, "gdi32.lib"); 31 32 import windows.windef; 33 import windows.winuser; 34 import windows.wingdi; 35 36 alias std.algorithm.min min; 37 alias std.algorithm.max max; 38 39 import cairo.cairo; 40 import cairo.win32; 41 42 alias cairo.cairo.RGB RGB; 43 44 import util.rounded_rectangle; 45 46 struct StateContext 47 { 48 Context ctx; 49 50 this(Context ctx) 51 { 52 this.ctx = ctx; 53 ctx.save(); 54 } 55 56 ~this() 57 { 58 ctx.restore(); 59 } 60 61 alias ctx this; 62 } 63 64 /* Each allocation consumes 3 GDI objects. */ 65 class PaintBuffer 66 { 67 this(HDC localHdc, int cxClient, int cyClient) 68 { 69 width = cxClient; 70 height = cyClient; 71 72 hBuffer = CreateCompatibleDC(localHdc); 73 hBitmap = CreateCompatibleBitmap(localHdc, cxClient, cyClient); 74 hOldBitmap = SelectObject(hBuffer, hBitmap); 75 76 surf = new Win32Surface(hBuffer); 77 ctx = Context(surf); 78 initialized = true; 79 } 80 81 ~this() 82 { 83 if (initialized) 84 { 85 clear(); 86 } 87 } 88 89 void clear() 90 { 91 SelectObject(hBuffer, hOldBitmap); 92 DeleteObject(hBitmap); 93 DeleteDC(hBuffer); 94 95 //~ surf.dispose(); 96 //~ ctx.dispose(); 97 //~ surf.finish(); 98 99 initialized = false; 100 } 101 102 bool initialized; 103 int width, height; 104 HDC hBuffer; 105 HBITMAP hBitmap; 106 HBITMAP hOldBitmap; 107 Context ctx; 108 Surface surf; 109 } 110 111 HANDLE makeWindow(HWND hwnd, int childID = 1, string classname = WidgetClass, string description = null) 112 { 113 return CreateWindow(classname.toUTF16z, description.toUTF16z, 114 WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN, // WS_CLIPCHILDREN is necessary 115 0, 0, 0, 0, // Size!int and Position are set by MoveWindow 116 hwnd, cast(HANDLE)childID, // child ID 117 cast(HINSTANCE)GetWindowLongPtr(hwnd, GWL_HINSTANCE), // hInstance 118 null); 119 } 120 121 enum RoundingMethod = RoundMethod.C; 122 123 abstract class Widget 124 { 125 Widget parent; 126 PaintBuffer paintBuffer; 127 PAINTSTRUCT ps; 128 129 HWND hwnd; 130 int width, height; 131 bool needsRedraw = true; 132 133 this(Widget parent, HWND hwnd, int width, int height) 134 { 135 this.parent = parent; 136 this.hwnd = hwnd; 137 this.width = width; 138 this.height = height; 139 } 140 141 @property Size!int size() 142 { 143 return Size!int(width, height); 144 } 145 146 abstract LRESULT process(UINT message, WPARAM wParam, LPARAM lParam) 147 { 148 switch (message) 149 { 150 case WM_ERASEBKGND: 151 { 152 return 1; 153 } 154 155 case WM_PAINT: 156 { 157 OnPaint(hwnd, message, wParam, lParam); 158 return 0; 159 } 160 161 case WM_SIZE: 162 { 163 width = LOWORD(lParam); 164 height = HIWORD(lParam); 165 166 auto localHdc = GetDC(hwnd); 167 168 if (paintBuffer !is null) 169 { 170 paintBuffer.clear(); 171 } 172 173 paintBuffer = new PaintBuffer(localHdc, width, height); 174 ReleaseDC(hwnd, localHdc); 175 176 needsRedraw = true; 177 blit(); 178 return 0; 179 } 180 181 case WM_TIMER: 182 { 183 blit(); 184 return 0; 185 } 186 187 case WM_DESTROY: 188 { 189 // @BUG@ 190 // Not doing this here causes exceptions being thrown from within cairo 191 // when calling surface.dispose(). I'm not sure why yet. 192 paintBuffer.clear(); 193 return 0; 194 } 195 196 default: 197 } 198 199 return DefWindowProc(hwnd, message, wParam, lParam); 200 } 201 202 void redraw() 203 { 204 needsRedraw = true; 205 blit(); 206 } 207 208 void blit() 209 { 210 InvalidateRect(hwnd, null, true); 211 } 212 213 void OnPaint(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 214 { 215 auto ctx = &paintBuffer.ctx; 216 auto hBuffer = paintBuffer.hBuffer; 217 auto hdc = BeginPaint(hwnd, &ps); 218 auto boundRect = ps.rcPaint; 219 220 if (needsRedraw) 221 { 222 draw(StateContext(*ctx)); 223 needsRedraw = false; 224 } 225 226 with (boundRect) 227 { 228 BitBlt(hdc, left, top, right - left, bottom - top, hBuffer, left, top, SRCCOPY); 229 } 230 231 EndPaint(hwnd, &ps); 232 } 233 234 abstract void draw(StateContext ctx); 235 } 236 237 class StepWidget : Widget 238 { 239 static size_t curBeat; 240 static bool selectState; 241 242 this(Widget parent, HWND hwnd, int width, int height) 243 { 244 super(parent, hwnd, width, height); 245 } 246 } 247 248 class Step : StepWidget 249 { 250 bool _selected; 251 size_t beatIndex; 252 253 @property void selected(bool state) 254 { 255 _selected = state; 256 redraw(); 257 } 258 259 @property bool selected() 260 { 261 return _selected; 262 } 263 264 this(Widget parent, size_t beatIndex, HWND hwnd, int width, int height) 265 { 266 this.beatIndex = beatIndex; 267 super(parent, hwnd, width, height); 268 } 269 270 override LRESULT process(UINT message, WPARAM wParam, LPARAM lParam) 271 { 272 switch (message) 273 { 274 case WM_LBUTTONDOWN: 275 { 276 selected = !selected; 277 selectState = selected; 278 break; 279 } 280 281 case WM_MOUSEMOVE: 282 { 283 if (wParam & MK_LBUTTON) 284 { 285 selected = selectState; 286 } 287 288 break; 289 } 290 291 default: 292 } 293 294 return super.process(message, wParam, lParam); 295 } 296 297 override void draw(StateContext ctx) 298 { 299 ctx.rectangle(1, 1, width - 2, height - 2); 300 ctx.setSourceRGB(0, 0, 0.8); 301 ctx.fill(); 302 303 if (selected) 304 { 305 auto darkCyan = RGB(0, 0.6, 1); 306 ctx.setSourceRGB(darkCyan); 307 int index, xPos, yPos; 308 309 // @BUG@ 6569: alias this not usable with template overloads, cast needed. 310 roundedRectangle!(RoundingMethod)(cast(Context)ctx, 5.0, 5.0, width - 10.0, height - 10.0, 15.0); 311 ctx.fill(); 312 313 if (beatIndex == curBeat) 314 { 315 ctx.setSourceRGB(1, 1, 0); 316 } 317 else 318 { 319 ctx.setSourceRGB(brightness(darkCyan, + 0.4)); 320 } 321 322 roundedRectangle!(RoundingMethod)(cast(Context)ctx, 10, 10, width - 20, height - 20, 15); 323 ctx.fill(); 324 } 325 } 326 } 327 328 class TimeStep : StepWidget 329 { 330 size_t beatIndex; 331 332 this(Widget parent, size_t beatIndex, HWND hwnd, int width, int height) 333 { 334 this.beatIndex = beatIndex; 335 super(parent, hwnd, width, height); 336 } 337 338 override LRESULT process(UINT message, WPARAM wParam, LPARAM lParam) 339 { 340 return super.process(message, wParam, lParam); 341 } 342 343 override void draw(StateContext ctx) 344 { 345 ctx.rectangle(1, 1, width - 2, height - 2); 346 ctx.setSourceRGB(0, 0, 0.6); 347 ctx.fill(); 348 349 if (beatIndex == curBeat) 350 { 351 auto darkBlue = RGB(0, 0, 0.8); 352 ctx.setSourceRGB(darkBlue); 353 roundedRectangle!(RoundingMethod)(cast(Context)ctx, 5, 5, width - 10, height - 10, 15); 354 ctx.fill(); 355 356 ctx.setSourceRGB(brightness(darkBlue, + 0.2)); 357 roundedRectangle!(RoundingMethod)(cast(Context)ctx, 10, 10, width - 20, height - 20, 15); 358 ctx.fill(); 359 } 360 } 361 } 362 363 class Steps : Widget 364 { 365 enum BeatCount = 6; // horizontal steps 366 enum StepCount = 6; // vertical steps 367 368 TimeStep[BeatCount] timeSteps; 369 Step[StepCount][BeatCount] steps; 370 size_t curBeat; 371 372 int stepWidth; 373 int stepHeight; 374 375 // note: the main window is still not a Widget class, so parent is null 376 this(Widget parent, HWND hwnd, int width, int height) 377 { 378 super(parent, hwnd, width, height); 379 380 stepWidth = width / BeatCount; 381 stepHeight = height / 10; 382 383 createWidgets!TimeStep(); 384 createWidgets!Step(stepHeight); 385 386 StepWidget.curBeat = curBeat; 387 388 enum TimerID = 500; 389 SetTimer(hwnd, TimerID, 1000 / 4, null); 390 } 391 392 // todo: figure out how to remove code duplication here. The root cause is 393 // the 1 vs 2 foreach loops. 394 void createWidgets(WidgetType)(size_t vOffset = 0) if (is(WidgetType == TimeStep)) 395 { 396 foreach (beatIndex; 0 .. BeatCount) 397 { 398 auto hWindow = makeWindow(hwnd); 399 auto widget = new WidgetType(this, beatIndex, hWindow, stepWidth, stepHeight); 400 WidgetHandles[hWindow] = widget; 401 timeSteps[beatIndex] = widget; 402 403 auto size = widget.size; 404 MoveWindow(hWindow, beatIndex * stepWidth, 0, size.width, size.height, true); 405 } 406 } 407 408 void createWidgets(WidgetType)(size_t vOffset = 0) if (is(WidgetType == Step)) 409 { 410 foreach (beatIndex; 0 .. BeatCount) 411 { 412 foreach (stepIndex; 0 .. StepCount) 413 { 414 auto hWindow = makeWindow(hwnd); 415 auto widget = new WidgetType(this, beatIndex, hWindow, stepWidth, stepHeight); 416 WidgetHandles[hWindow] = widget; 417 steps[beatIndex][stepIndex] = widget; 418 419 auto size = widget.size; 420 MoveWindow(hWindow, beatIndex * stepWidth, vOffset + (stepIndex * stepHeight), size.width, size.height, true); 421 } 422 } 423 } 424 425 void redrawSteps(size_t beatIndex) 426 { 427 timeSteps[beatIndex].redraw(); 428 foreach (step; steps[beatIndex]) 429 step.redraw(); 430 } 431 432 override LRESULT process(UINT message, WPARAM wParam, LPARAM lParam) 433 { 434 switch (message) 435 { 436 case WM_TIMER: 437 { 438 redrawSteps(curBeat); 439 440 curBeat = (curBeat + 1) % BeatCount; 441 StepWidget.curBeat = curBeat; 442 443 redrawSteps(curBeat); 444 return 0; 445 } 446 447 default: 448 } 449 450 return super.process(message, wParam, lParam); 451 } 452 453 override void draw(StateContext ctx) 454 { 455 ctx.setSourceRGB(1, 1, 1); 456 ctx.paint(); 457 } 458 } 459 460 /* A place to hold Widget objects. Since each window has a unique HWND, 461 * we can use this hash type to store references to Widgets and call 462 * their window processing methods. 463 */ 464 Widget[HWND] WidgetHandles; 465 466 /* 467 * All Widget windows have this window procedure registered via RegisterClass(), 468 * we use it to dispatch to the appropriate Widget window processing method. 469 * 470 * A similar technique is used in the DFL and DGUI libraries for all of its 471 * windows and widgets. 472 */ 473 extern (Windows) 474 LRESULT winDispatch(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 475 { 476 auto widget = hwnd in WidgetHandles; 477 478 if (widget !is null) 479 { 480 return widget.process(message, wParam, lParam); 481 } 482 483 return DefWindowProc(hwnd, message, wParam, lParam); 484 } 485 486 extern (Windows) 487 LRESULT mainWinProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 488 { 489 static PaintBuffer paintBuffer; 490 static int width, height; 491 492 void draw(StateContext ctx) 493 { 494 ctx.setSourceRGB(1, 1, 1); 495 ctx.paint(); 496 } 497 498 switch (message) 499 { 500 case WM_CREATE: 501 { 502 auto hDesk = GetDesktopWindow(); 503 RECT rc; 504 GetClientRect(hDesk, &rc); 505 506 auto localHdc = GetDC(hwnd); 507 paintBuffer = new PaintBuffer(localHdc, rc.right, rc.bottom); 508 509 auto hWindow = makeWindow(hwnd); 510 auto widget = new Steps(null, hWindow, 400, 400); 511 WidgetHandles[hWindow] = widget; 512 513 auto size = widget.size; 514 MoveWindow(hWindow, 0, 0, size.width, size.height, true); 515 516 return 0; 517 } 518 519 case WM_LBUTTONDOWN: 520 { 521 SetFocus(hwnd); 522 return 0; 523 } 524 525 case WM_SIZE: 526 { 527 width = LOWORD(lParam); 528 height = HIWORD(lParam); 529 return 0; 530 } 531 532 case WM_PAINT: 533 { 534 auto ctx = paintBuffer.ctx; 535 auto hBuffer = paintBuffer.hBuffer; 536 PAINTSTRUCT ps; 537 auto hdc = BeginPaint(hwnd, &ps); 538 auto boundRect = ps.rcPaint; 539 540 draw(StateContext(paintBuffer.ctx)); 541 542 with (boundRect) 543 { 544 BitBlt(hdc, left, top, right - left, bottom - top, paintBuffer.hBuffer, left, top, SRCCOPY); 545 } 546 547 EndPaint(hwnd, &ps); 548 return 0; 549 } 550 551 case WM_TIMER: 552 { 553 InvalidateRect(hwnd, null, true); 554 return 0; 555 } 556 557 case WM_MOUSEWHEEL: 558 { 559 return 0; 560 } 561 562 case WM_DESTROY: 563 { 564 PostQuitMessage(0); 565 return 0; 566 } 567 568 default: 569 } 570 571 return DefWindowProc(hwnd, message, wParam, lParam); 572 } 573 574 string WidgetClass = "WidgetClass"; 575 576 int myWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow) 577 { 578 string appName = "Step Sequencer"; 579 580 HWND hwnd; 581 MSG msg; 582 WNDCLASS wndclass; 583 584 /* One class for the main window */ 585 wndclass.lpfnWndProc = &mainWinProc; 586 wndclass.cbClsExtra = 0; 587 wndclass.cbWndExtra = 0; 588 wndclass.hInstance = hInstance; 589 wndclass.hIcon = LoadIcon(null, IDI_APPLICATION); 590 wndclass.hCursor = LoadCursor(null, IDC_ARROW); 591 wndclass.hbrBackground = null; 592 wndclass.lpszMenuName = null; 593 wndclass.lpszClassName = appName.toUTF16z; 594 595 if (!RegisterClass(&wndclass)) 596 { 597 MessageBox(null, "This program requires Windows NT!", appName.toUTF16z, MB_ICONERROR); 598 return 0; 599 } 600 601 /* Separate window class for Widgets. */ 602 wndclass.hbrBackground = null; 603 wndclass.lpfnWndProc = &winDispatch; 604 wndclass.cbWndExtra = 0; 605 wndclass.hIcon = null; 606 wndclass.lpszClassName = WidgetClass.toUTF16z; 607 608 if (!RegisterClass(&wndclass)) 609 { 610 MessageBox(null, "This program requires Windows NT!", appName.toUTF16z, MB_ICONERROR); 611 return 0; 612 } 613 614 hwnd = CreateWindow(appName.toUTF16z, "step sequencer", 615 WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, // WS_CLIPCHILDREN is necessary 616 cast(int)(1680 / 3.3), 1050 / 3, 617 400, 400, 618 null, null, hInstance, null); 619 620 ShowWindow(hwnd, iCmdShow); 621 UpdateWindow(hwnd); 622 623 while (GetMessage(&msg, null, 0, 0)) 624 { 625 TranslateMessage(&msg); 626 DispatchMessage(&msg); 627 } 628 629 return msg.wParam; 630 } 631 632 extern (Windows) 633 int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow) 634 { 635 int result; 636 637 638 try 639 { 640 Runtime.initialize(); 641 myWinMain(hInstance, hPrevInstance, lpCmdLine, iCmdShow); 642 Runtime.terminate(); 643 } 644 catch (Throwable o) 645 { 646 MessageBox(null, o.toString().toUTF16z, "Error", MB_OK | MB_ICONEXCLAMATION); 647 result = -1; 648 } 649 650 return result; 651 }