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