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 }