/win32_steps_extended.d

http://github.com/AndrejMitrovic/cairoDSamples · D · 668 lines · 513 code · 126 blank · 29 comment · 24 complexity · c7b35479451b15a5f92e6424d11020ec MD5 · raw file

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