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 }