1 module slider;
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  + This is an example of a custom Slider widget implemented with
12  + CairoD with the help of the Win32 API.
13  + You can change the orientation of the slider, its colors,
14  + its maximum value and the extra padding that allows selection
15  + of the slider within extra bounds.
16  +
17  + Currently it doesn't instantiate multiple sliders.
18  +
19  + It relies heavily on Win32 features, such as widgets being
20  + implemented as windows (windows, buttons, menus, and even the
21  + desktop window are all windows) which have X and Y points relative
22  + to their own window, the ability to capture the mouse position
23  + even when it goes outside a window area, and some other features.
24  +/
25 
26 import core.memory;
27 import core.runtime;
28 import core.thread;
29 import core.stdc.config;
30 
31 import std.algorithm;
32 import std.array;
33 import std.conv;
34 import std.exception;
35 import std.functional;
36 import std.math;
37 import std.random;
38 import std.range;
39 import std.stdio;
40 import std.string;
41 import std.traits;
42 import std.utf;
43 
44 pragma(lib, "gdi32.lib");
45 
46 import windows.windef;
47 import windows.winuser;
48 import windows.wingdi;
49 
50 alias std.algorithm.min min;  // conflict resolution
51 alias std.algorithm.max max;  // conflict resolution
52 
53 import cairo.c.cairo;
54 import cairo.cairo;
55 import cairo.win32;
56 
57 alias cairo.cairo.RGB RGB;  // conflict resolution
58 
59 /*
60  * These should be tracked in one place for the entire app,
61  * otherwise you might end up having multiple widgets with
62  * their own control/shift key states.
63  */
64 __gshared bool shiftState;
65 __gshared bool controlState;
66 __gshared bool mouseTrack;
67 
68 /* Used in painting the slider background/foreground/thumb */
69 struct SliderColors
70 {
71     RGBA thumbActive;
72     RGBA thumbHover;
73     RGBA thumbIdle;
74 
75     RGBA fill;
76     RGBA back;
77     RGBA window;
78 }
79 
80 /* Two types of sliders */
81 enum Axis
82 {
83     vertical,
84     horizontal
85 }
86 
87 class SliderWindow
88 {
89     int cxClient, cyClient;  /* width, height */
90     HWND hwnd;
91     RGBA thumbColor;      // current active thumb color
92     Axis axis;            // slider orientation
93     SliderColors colors;  // initialized colors
94     int thumbPos;
95     int size;
96     int thumbSize;
97     int offset;           // since round caps add additional pixels, we offset the drawing
98     int step;             // used when control key is held
99     bool isActive;
100 
101     this(HWND hwnd, Axis axis, int size, int thumbSize, int padding, SliderColors colors)
102     {
103         this.hwnd = hwnd;
104         this.colors = colors;
105         this.axis = axis;
106         this.size = size;
107         this.thumbSize = thumbSize;
108         offset = padding / 2;
109 
110         thumbPos = (axis == Axis.horizontal) ? 0 : size - thumbSize;
111         step     = (axis == Axis.horizontal) ? 2 : -2;
112     }
113 
114     /* Get the mouse offset based on slider orientation */
115     short getTrackPos(LPARAM lParam)
116     {
117         final switch (axis)
118         {
119             case Axis.vertical:
120             {
121                 return cast(short)HIWORD(lParam);
122             }
123 
124             case Axis.horizontal:
125             {
126                 return cast(short)LOWORD(lParam);
127             }
128         }
129     }
130 
131     /* Flips x and y based on slider orientation */
132     void axisFlip(ref int x, ref int y)
133     {
134         final switch (axis)
135         {
136             case Axis.vertical:
137             {
138                 break;
139             }
140 
141             case Axis.horizontal:
142             {
143                 std.algorithm.swap(x, y);
144                 break;
145             }
146         }
147     }
148 
149     /* Update the thumb position based on mouse position */
150     void mouseTrackPos(LPARAM lParam)
151     {
152         auto trackPos = getTrackPos(lParam);
153 
154         /* steps:
155          * 1. compensate for offseting the slider drawing
156          * (we do not draw from Point(0, 0) because round caps add more pixels).
157          * 2. position the thumb so its center is at the mouse cursor position.
158          * 3. limit the final value between the minimum and maximum position.
159          */
160         thumbPos = (max(0, min(trackPos - offset - (thumbSize / 2), size)));
161     }
162 
163     /* Get the neutral value (calculated based on axis orientation.) */
164     int getValue()
165     {
166         final switch (axis)
167         {
168             // vertical sliders have a more natural minimum position at the bottom,
169             // and since the Y axis increases towards the bottom we have to invert
170             // this value.
171             case Axis.vertical:
172             {
173                 return (retro(iota(0, size + 1)))[thumbPos];
174             }
175 
176             case Axis.horizontal:
177             {
178                 return thumbPos;
179             }
180         }
181     }
182 
183     /* Process window messages for this slider */
184     LRESULT process(UINT message, WPARAM wParam, LPARAM lParam)
185     {
186         HDC hdc;
187         PAINTSTRUCT ps;
188         RECT rc;
189         HDC  _buffer;
190         HBITMAP hBitmap;
191         HBITMAP hOldBitmap;
192         switch (message)
193         {
194             case WM_SIZE:
195             {
196                 cxClient = LOWORD(lParam);
197                 cyClient = HIWORD(lParam);
198                 InvalidateRect(hwnd, null, FALSE);
199                 return 0;
200             }
201 
202             /* We're selected, capture the mouse and track its position,
203              * focus our window (this unfocuses all other windows).
204              */
205             case WM_LBUTTONDOWN:
206             {
207                 mouseTrackPos(lParam);
208                 SetCapture(hwnd);
209                 mouseTrack = true;
210                 SetFocus(hwnd);
211 
212                 InvalidateRect(hwnd, null, FALSE);
213                 return 0;
214             }
215 
216             /*
217              * End any mouse tracking.
218              */
219             case WM_LBUTTONUP:
220             {
221                 if (mouseTrack)
222                 {
223                     ReleaseCapture();
224                     mouseTrack = false;
225                 }
226 
227                 InvalidateRect(hwnd, null, FALSE);
228                 return 0;
229             }
230 
231             /*
232              * We're focused, change slider settings.
233              */
234             case WM_SETFOCUS:
235             {
236                 isActive = true;
237                 thumbColor = colors.thumbActive;
238                 InvalidateRect(hwnd, null, FALSE);
239                 return 0;
240             }
241 
242             /*
243              * We've lost focus, change slider settings.
244              */
245             case WM_KILLFOCUS:
246             {
247                 isActive = false;
248                 thumbColor = colors.thumbIdle;
249                 InvalidateRect(hwnd, null, FALSE);
250                 return 0;
251             }
252 
253             /*
254              * If we're tracking the mouse update the
255              * slider thumb position.
256              */
257             case WM_MOUSEMOVE:
258             {
259                 if (mouseTrack)  // move thumb
260                 {
261                     //~ writeln(getValue());
262                     mouseTrackPos(lParam);
263                     InvalidateRect(hwnd, null, FALSE);
264                 }
265 
266                 return 0;
267             }
268 
269             /*
270              * Mouse wheel can control the slider position too.
271              */
272             case WM_MOUSEWHEEL:
273             {
274                 if (isActive)
275                 {
276                     OnMouseWheel(cast(short)HIWORD(wParam));
277                     InvalidateRect(hwnd, null, FALSE);
278                 }
279 
280                 return 0;
281             }
282 
283             /*
284              * Various keys such as Up/Down/Left/Right/PageUp/
285              * PageDown/Tab/ and the Shift state can control
286              * the slider thumb position.
287              */
288             case WM_KEYDOWN:
289             case WM_KEYUP:
290             case WM_CHAR:
291             case WM_DEADCHAR:
292             case WM_SYSKEYDOWN:
293             case WM_SYSKEYUP:
294             case WM_SYSCHAR:
295             case WM_SYSDEADCHAR:
296             {
297                 // message: key state, wParam: key ID
298                 keyUpdate(message, wParam);
299                 InvalidateRect(hwnd, null, FALSE);
300                 return 0;
301             }
302 
303             /*
304              * The paint routine recreates the Cairo context and double-buffering
305              * mechanism on each WM_PAINT message. You could set up context recreation
306              * only when it's necessary (e.g. on a WM_SIZE message), however this quickly
307              * gets complicated due to Cairo's stateful API.
308              */
309             case WM_PAINT:
310             {
311                 hdc = BeginPaint(hwnd, &ps);
312                 GetClientRect(hwnd, &rc);
313 
314                 auto left   = rc.left;
315                 auto top    = rc.top;
316                 auto right  = rc.right;
317                 auto bottom = rc.bottom;
318 
319                 auto width  = right - left;
320                 auto height = bottom - top;
321                 auto x      = left;
322                 auto y      = top;
323 
324                 /* Double buffering */
325                 _buffer    = CreateCompatibleDC(hdc);
326                 hBitmap    = CreateCompatibleBitmap(hdc, width, height);
327                 hOldBitmap = SelectObject(_buffer, hBitmap);
328 
329                 auto surf = new Win32Surface(_buffer);
330                 auto ctx  = Context(surf);
331 
332                 drawSlider(ctx);
333 
334                 // Blit the texture to the screen
335                 BitBlt(hdc, 0, 0, width, height, _buffer, x, y, SRCCOPY);
336 
337                 surf.finish();
338                 surf.dispose();
339                 ctx.dispose();
340 
341                 SelectObject(_buffer, hOldBitmap);
342                 DeleteObject(hBitmap);
343                 DeleteDC(_buffer);
344 
345                 EndPaint(hwnd, &ps);
346 
347                 return 0;
348             }
349 
350             default:
351         }
352 
353         return DefWindowProc(hwnd, message, wParam, lParam);
354     }
355 
356     void drawSlider(Context ctx)
357     {
358         /* window backround */
359         ctx.setSourceRGBA(colors.window);
360         ctx.paint();
361 
362         ctx.translate(offset, offset);
363 
364         ctx.setLineWidth(10);
365         ctx.setLineCap(LineCap.CAIRO_LINE_CAP_ROUND);
366 
367         /* slider backround */
368         auto begX = 0;
369         auto begY = 0;
370         auto endX = 0;
371         auto endY = size + thumbSize;
372 
373         axisFlip(begX, begY);
374         axisFlip(endX, endY);
375 
376         ctx.setSourceRGBA(colors.back);
377         ctx.moveTo(begX, begY);
378         ctx.lineTo(endX, endY);
379         ctx.stroke();
380 
381         /* slider value fill */
382         begX = 0;
383         // vertical sliders have a minimum position at the bottom.
384         begY = (axis == Axis.horizontal) ? 0 : size + thumbSize;
385         endX = 0;
386         endY = thumbPos + thumbSize;
387 
388         axisFlip(begX, begY);
389         axisFlip(endX, endY);
390 
391         ctx.setSourceRGBA(colors.fill);
392         ctx.moveTo(begX, begY);
393         ctx.lineTo(endX, endY);
394         ctx.stroke();
395 
396         /* slider thumb */
397         begX = 0;
398         begY = thumbPos;
399         endX = 0;
400         endY = thumbPos + thumbSize;
401 
402         axisFlip(begX, begY);
403         axisFlip(endX, endY);
404 
405         ctx.setSourceRGBA(thumbColor);
406         ctx.moveTo(begX, begY);
407         ctx.lineTo(endX, endY);
408         ctx.stroke();
409     }
410 
411     /*
412      * Process various keys.
413      * This function is continuously called when a key is held,
414      * but we only update the slider position when a key is pressed
415      * down (WM_KEYDOWN), and not when it's released.
416      */
417     void keyUpdate(UINT keyState, WPARAM wParam)
418     {
419         switch (wParam)
420         {
421             case VK_LEFT:
422             {
423                 if (keyState == WM_KEYDOWN)
424                 {
425                     if (controlState)
426                         thumbPos -= step * 2;
427                     else
428                         thumbPos -= step;
429                 }
430                 break;
431             }
432 
433             case VK_RIGHT:
434             {
435                 if (keyState == WM_KEYDOWN)
436                 {
437                     if (controlState)
438                         thumbPos += step * 2;
439                     else
440                         thumbPos += step;
441                 }
442                 break;
443             }
444 
445             case VK_SHIFT:
446             {
447                 shiftState = (keyState == WM_KEYDOWN);
448                 break;
449             }
450 
451             case VK_CONTROL:
452             {
453                 controlState = (keyState == WM_KEYDOWN);
454                 break;
455             }
456 
457             case VK_UP:
458             {
459                 if (keyState == WM_KEYDOWN)
460                 {
461                     if (controlState)
462                         thumbPos += step * 2;
463                     else
464                         thumbPos += step;
465                 }
466                 break;
467             }
468 
469             case VK_DOWN:
470             {
471                 if (keyState == WM_KEYDOWN)
472                 {
473                     if (controlState)
474                         thumbPos -= step * 2;
475                     else
476                         thumbPos -= step;
477                 }
478                 break;
479             }
480 
481             case VK_HOME:
482             {
483                 thumbPos = 0;
484                 break;
485             }
486 
487             case VK_END:
488             {
489                 thumbPos = size;
490                 break;
491             }
492 
493             case VK_PRIOR:  // page up
494             {
495                 thumbPos += step * 2;
496                 break;
497             }
498 
499             case VK_NEXT:  // page down
500             {
501                 thumbPos -= step * 2;
502                 break;
503             }
504 
505             /*
506              * this can be used to switch between different slider windows.
507              * However this should ideally be handled in a main window, not here.
508              * We could pass this message back to the main window.
509              * Currently unimplemented.
510              */
511             case VK_TAB:
512             {
513                 //~ if (shiftState)  // shift+tab means go the other way
514                     //~ SetFocus(slidersRange.back);
515                 //~ else
516                     //~ SetFocus(slidersRange.front);
517                 //~ break;
518             }
519 
520             default:
521         }
522 
523         // normalize the thumb position
524         thumbPos = (max(0, min(thumbPos, size)));
525     }
526 
527     void OnMouseWheel(sizediff_t nDelta)
528     {
529         if (-nDelta/120 > 0)
530             thumbPos -= step;
531         else
532             thumbPos += step;
533 
534         thumbPos = (max(0, min(thumbPos, size)));
535     }
536 }
537 
538 /*
539  * A place to hold Slider objects. Since each window has a unique HWND,
540  * we can use this hash type to store references to objects and call
541  * their window processing methods.
542  */
543 SliderWindow[HWND] SliderHandles;
544 
545 /*
546  * All Slider windows will have this same window procedure registered via
547  * RegisterClass(), we use it to dispatch to the appropriate class window
548  * processing method. We could also place this inside the Slider class as
549  * a static method, however this kind of dispatch function is actually
550  * useful for dispatching to any number and types of windows.
551  *
552  * A similar technique is used in the DFL and DGUI libraries for all of its
553  * windows and widgets.
554  */
555 extern (Windows)
556 LRESULT winDispatch(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
557 {
558     auto slider = hwnd in SliderHandles;
559 
560     if (slider !is null)
561     {
562         return slider.process(message, wParam, lParam);
563     }
564 
565     return DefWindowProc(hwnd, message, wParam, lParam);
566 }
567 
568 extern (Windows)
569 LRESULT mainWinProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
570 {
571     static int size = 100;
572     static int padding = 20;
573     static int thumbSize = 20;
574     static HWND hSlider;
575     static axis = Axis.vertical;  /* vertical or horizontal orientation */
576 
577     switch (message)
578     {
579         case WM_CREATE:
580         {
581             hSlider = CreateWindow(sliderClass.toUTF16z, null,
582                       WS_CHILDWINDOW | WS_VISIBLE,
583                       0, 0, 0, 0,
584                       hwnd,
585                       cast(HMENU)(0),                                  // child ID
586                       cast(HINSTANCE)GetWindowLongPtr(hwnd, GWL_HINSTANCE),  // hInstance
587                       null);
588 
589             SliderColors colors;
590             with (colors)
591             {
592                 thumbActive = RGBA(1, 1, 1, 1);
593                 thumbHover  = RGBA(1, 1, 0, 1);
594                 thumbIdle   = RGBA(1, 0, 0, 1);
595 
596                 fill        = RGBA(1, 0, 0, 1);
597                 back        = RGBA(1, 0, 0, 0.5);
598                 window      = RGBA(0, 0, 0, 0);
599             }
600 
601             SliderHandles[hSlider] = new SliderWindow(hSlider, axis, size, thumbSize, padding, colors);
602             return 0;
603         }
604 
605         /* The main window creates the child window and has to set the position and size. */
606         case WM_SIZE:
607         {
608             auto sliderWidth  = size + padding + thumbSize;
609             auto sliderHeight = padding;
610 
611             if (axis == Axis.vertical)
612                 std.algorithm.swap(sliderWidth, sliderHeight);
613 
614             MoveWindow(hSlider, 0, 0, sliderWidth, sliderHeight, true);
615             return 0;
616         }
617 
618         /* Focus main window, this kills any active child window focus. */
619         case WM_LBUTTONDOWN:
620         {
621             SetFocus(hwnd);
622             return 0;
623         }
624 
625         case WM_KEYDOWN:
626         {
627             if (wParam == VK_ESCAPE)
628                 goto case WM_DESTROY;
629 
630             return 0;
631         }
632 
633         case WM_DESTROY:
634         {
635             PostQuitMessage(0);
636             return 0;
637         }
638 
639         default:
640     }
641 
642     return DefWindowProc(hwnd, message, wParam, lParam);
643 }
644 
645 string sliderClass = "SliderClass";
646 
647 int myWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow)
648 {
649     string appName = "sliders";
650 
651     HWND hwnd;
652     MSG  msg;
653     WNDCLASS wndclass;
654 
655     wndclass.style       = CS_HREDRAW | CS_VREDRAW;
656     wndclass.lpfnWndProc = &mainWinProc;
657     wndclass.cbClsExtra  = 0;
658     wndclass.cbWndExtra  = 0;
659     wndclass.hInstance   = hInstance;
660     wndclass.hIcon       = LoadIcon(null, IDI_APPLICATION);
661     wndclass.hCursor     = LoadCursor(null, IDC_ARROW);
662     wndclass.hbrBackground = null;  // todo: replace with null, paint bg with cairo
663     wndclass.lpszMenuName  = null;
664     wndclass.lpszClassName = appName.toUTF16z;
665 
666     if (!RegisterClass(&wndclass))
667     {
668         MessageBox(null, "This program requires Windows NT!", appName.toUTF16z, MB_ICONERROR);
669         return 0;
670     }
671 
672     /* Separate window class for all widgets, in this case only Sliders. */
673     wndclass.hbrBackground = null;
674     wndclass.lpfnWndProc   = &winDispatch;
675     wndclass.cbWndExtra    = 0;
676     wndclass.hIcon         = null;
677     wndclass.lpszClassName = sliderClass.toUTF16z;
678 
679     if (!RegisterClass(&wndclass))
680     {
681         MessageBox(null, "This program requires Windows NT!", appName.toUTF16z, MB_ICONERROR);
682         return 0;
683     }
684 
685     hwnd = CreateWindow(appName.toUTF16z, "sliders example",
686                         WS_OVERLAPPEDWINDOW,
687                         400, 400,
688                         50, 200,
689                         null, null, hInstance, null);
690 
691     ShowWindow(hwnd, iCmdShow);
692     UpdateWindow(hwnd);
693 
694     while (GetMessage(&msg, null, 0, 0))
695     {
696         TranslateMessage(&msg);
697         DispatchMessage(&msg);
698     }
699 
700     return msg.wParam;
701 }
702 
703 extern (Windows)
704 int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow)
705 {
706     int result;
707 
708     try
709     {
710         Runtime.initialize();
711         result = myWinMain(hInstance, hPrevInstance, lpCmdLine, iCmdShow);
712         Runtime.terminate();
713     }
714     catch (Throwable o)
715     {
716         MessageBox(null, o.toString().toUTF16z, "Error", MB_OK | MB_ICONEXCLAMATION);
717         result = 0;
718     }
719 
720     return result;
721 }