1 module scrolling;
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 import core.memory;
11 import core.runtime;
12 import core.thread;
13 import core.stdc.config;
14 
15 import std.algorithm;
16 import std.array;
17 import std.conv;
18 import std.exception;
19 import std.functional;
20 import std.math;
21 import std.random;
22 import std.range;
23 import std.stdio;
24 import std.string;
25 import std.traits;
26 import std.utf;
27 
28 pragma(lib, "gdi32.lib");
29 
30 import windows.windef;
31 import windows.winuser;
32 import windows.wingdi;
33 
34 alias std.algorithm.min min;  // conflict resolution
35 alias std.algorithm.max max;  // conflict resolution
36 
37 import cairo.cairo;
38 import cairo.win32;
39 
40 alias cairo.cairo.RGB RGB;   // conflict resolution
41 
42 struct StateContext
43 {
44     Context ctx;
45 
46     this(Context ctx)
47     {
48         this.ctx = ctx;
49         ctx.save();
50     }
51 
52     ~this()
53     {
54         ctx.restore();
55     }
56 
57     alias ctx this;
58 }
59 
60 class PaintBuffer
61 {
62     this(HDC localHdc, int cxClient, int cyClient)
63     {
64         hdc    = localHdc;
65         width  = cxClient;
66         height = cyClient;
67 
68         hBuffer    = CreateCompatibleDC(localHdc);
69         hBitmap    = CreateCompatibleBitmap(localHdc, cxClient, cyClient);
70         hOldBitmap = SelectObject(hBuffer, hBitmap);
71 
72         surf = new Win32Surface(hBuffer);
73         ctx = Context(surf);
74         initialized = true;
75     }
76 
77     ~this()
78     {
79         if (initialized)
80         {
81             clear();
82         }
83     }
84 
85     void clear()
86     {
87         ctx.dispose();
88         surf.finish();
89         surf.dispose();
90 
91         SelectObject(hBuffer, hOldBitmap);
92         DeleteObject(hBitmap);
93         DeleteDC(hBuffer);
94         initialized = false;
95     }
96 
97     HDC hdc;
98     bool initialized;
99     int width, height;
100     HDC hBuffer;
101     HBITMAP hBitmap;
102     HBITMAP hOldBitmap;
103     Context ctx;
104     Surface surf;
105 }
106 
107 abstract class Widget
108 {
109     Widget parent;
110     PAINTSTRUCT ps;
111     PaintBuffer mainPaintBuff;
112     PaintBuffer paintBuffer;
113     HWND hwnd;
114     int width, height;
115     int xOffset, yOffset;
116     bool needsRedraw = true;
117     bool selected;
118 
119     this(HWND hwnd, int width, int height)
120     {
121         this.hwnd = hwnd;
122         this.width = width;
123         this.height = height;
124         //~ SetTimer(hwnd, 100, 1, null);
125     }
126 
127     @property Size!int size()
128     {
129         return Size!int(width, height);
130     }
131 
132     abstract LRESULT process(UINT message, WPARAM wParam, LPARAM lParam)
133     {
134         switch (message)
135         {
136             case WM_ERASEBKGND:
137             {
138                 return 1;
139             }
140 
141             case WM_PAINT:
142             {
143                 OnPaint(hwnd, message, wParam, lParam);
144                 return 0;
145             }
146 
147             case WM_SIZE:
148             {
149                 width  = cast(short)LOWORD(lParam);
150                 height = cast(short)HIWORD(lParam);
151 
152                 auto localHdc = GetDC(hwnd);
153 
154                 if (paintBuffer !is null)
155                 {
156                     paintBuffer.clear();
157                 }
158 
159                 paintBuffer = new PaintBuffer(localHdc, width, height);
160                 ReleaseDC(hwnd, localHdc);
161 
162                 needsRedraw = true;
163                 InvalidateRect(hwnd, null, true);
164                 return 0;
165             }
166 
167             case WM_TIMER:
168             {
169                 InvalidateRect(hwnd, null, true);
170                 return 0;
171             }
172 
173             case WM_MOVE:
174             {
175                 xOffset = cast(short)LOWORD(lParam);
176                 yOffset = cast(short)HIWORD(lParam);
177                 return 0;
178             }
179 
180             case WM_LBUTTONDOWN:
181             {
182                 selected ^= 1;  // flip
183                 needsRedraw = true;
184                 InvalidateRect(hwnd, null, false);
185                 return 0;
186             }
187 
188             case WM_DESTROY:
189             {
190                 paintBuffer.clear();
191                 PostQuitMessage(0);
192                 return 0;
193             }
194 
195             default:
196         }
197 
198         return DefWindowProc(hwnd, message, wParam, lParam);
199     }
200 
201     abstract void OnPaint(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
202     abstract void draw(StateContext ctx);
203 }
204 
205 class TestWidget2 : Widget
206 {
207     this(HWND hwnd, int width, int height)
208     {
209         super(hwnd, width, height);
210     }
211 
212     override LRESULT process(UINT message, WPARAM wParam, LPARAM lParam)
213     {
214         return super.process(message, wParam, lParam);
215     }
216 
217     override void OnPaint(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
218     {
219         auto ctx = paintBuffer.ctx;
220         auto hBuffer = paintBuffer.hBuffer;
221         auto hdc = BeginPaint(hwnd, &ps);
222         auto boundRect = ps.rcPaint;
223 
224         if (needsRedraw)
225         {
226             draw(StateContext(ctx));
227             needsRedraw = false;
228         }
229 
230         with (boundRect)
231         {
232             BitBlt(hdc, left, top, right - left, bottom - top, paintBuffer.hBuffer, left, top, SRCCOPY);
233         }
234 
235         EndPaint(hwnd, &ps);
236     }
237 
238     override void draw(StateContext ctx)
239     {
240         ctx.setSourceRGB(1, 1, 1);
241         ctx.paint();
242 
243         ctx.save();
244         ctx.scale(width, height);
245         ctx.moveTo(0, 0);
246 
247         ctx.rectangle(0, 0, 1, 1);
248         ctx.setSourceRGBA(1, 1, 1, 0);
249         ctx.setOperator(Operator.CAIRO_OPERATOR_CLEAR);
250         ctx.fill();
251 
252         ctx.setSourceRGB(0, 0, 0);
253         ctx.setOperator(Operator.CAIRO_OPERATOR_OVER);
254 
255         auto linpat = new LinearGradient(0, 0, 1, 1);
256         linpat.addColorStopRGB(0, RGB(0, 0.3, 0.8));
257         linpat.addColorStopRGB(1, RGB(0, 0.8, 0.3));
258 
259         auto radpat = new RadialGradient(0.5, 0.5, 0.25, 0.5, 0.5, 0.75);
260         radpat.addColorStopRGBA(0,   RGBA(0, 0, 0, 1));
261         radpat.addColorStopRGBA(0.5, RGBA(0, 0, 0, 0));
262 
263         ctx.setSource(linpat);
264         ctx.mask(radpat);
265         ctx.restore();
266 
267         if (selected)
268         {
269             ctx.moveTo(0, 0);
270             ctx.setLineWidth(3);
271             ctx.setSourceRGB(1, 0, 0);
272             ctx.rectangle(0, 0, width, height);
273             ctx.stroke();
274         }
275     }
276 }
277 
278 class TestWidget : Widget
279 {
280     RGB backColor;
281 
282     this(HWND hwnd, int width, int height)
283     {
284         super(hwnd, width, height);
285         this.backColor = RGB(1, 0, 0);
286 
287         auto localHdc = GetDC(hwnd);
288         auto hWindow = CreateWindow(WidgetClass.toUTF16z, null,
289                        WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN,  // WS_CLIPCHILDREN is necessary
290                        0, 0, 0, 0,
291                        hwnd, cast(HANDLE)1,                                   // child ID
292                        cast(HINSTANCE)GetWindowLongPtr(hwnd, GWL_HINSTANCE),  // hInstance
293                        null);
294 
295         auto widget = new TestWidget2(hWindow, width / 2, width / 2);
296         WidgetHandles[hWindow] = widget;
297 
298         auto size = widget.size;
299         MoveWindow(hWindow, size.width / 2, size.height / 2, size.width, size.height, true);
300     }
301 
302     override LRESULT process(UINT message, WPARAM wParam, LPARAM lParam)
303     {
304         return super.process(message, wParam, lParam);
305     }
306 
307     override void OnPaint(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
308     {
309         auto ctx = paintBuffer.ctx;
310         auto hBuffer = paintBuffer.hBuffer;
311         auto hdc = BeginPaint(hwnd, &ps);
312         auto boundRect = ps.rcPaint;
313 
314         if (needsRedraw)
315         {
316             draw(StateContext(ctx));
317             needsRedraw = false;
318         }
319 
320         with (boundRect)
321         {
322             BitBlt(hdc, left, top, right - left, bottom - top, paintBuffer.hBuffer, left, top, SRCCOPY);
323         }
324 
325         EndPaint(hwnd, &ps);
326     }
327 
328     override void draw(StateContext ctx)
329     {
330         ctx.save();
331         ctx.scale(width, height);
332         ctx.moveTo(0, 0);
333 
334         ctx.rectangle(0, 0, 1, 1);
335         ctx.setSourceRGBA(1, 1, 1, 0);
336         ctx.setOperator(Operator.CAIRO_OPERATOR_CLEAR);
337         ctx.fill();
338 
339         ctx.setSourceRGB(0, 0, 0);
340         ctx.setOperator(Operator.CAIRO_OPERATOR_OVER);
341 
342         auto linpat = new LinearGradient(0, 0, 1, 1);
343         linpat.addColorStopRGB(0, RGB(0, 0.3, 0.8));
344         linpat.addColorStopRGB(1, RGB(0, 0.8, 0.3));
345 
346         auto radpat = new RadialGradient(0.5, 0.5, 0.25, 0.5, 0.5, 0.75);
347         radpat.addColorStopRGBA(0,   RGBA(0, 0, 0, 1));
348         radpat.addColorStopRGBA(0.5, RGBA(0, 0, 0, 0));
349 
350         ctx.setSource(linpat);
351         ctx.mask(radpat);
352 
353         ctx.moveTo(0.1, 0.5);
354         ctx.restore();
355 
356         ctx.setSourceRGB(1, 1, 1);
357         ctx.selectFontFace("Tahoma", FontSlant.CAIRO_FONT_SLANT_NORMAL, FontWeight.CAIRO_FONT_WEIGHT_NORMAL);
358         ctx.setFontSize(20);
359         ctx.showText("weeeeeeeeeeeeeeeeeeeeeeeeeee");
360 
361         if (selected)
362         {
363             ctx.moveTo(0, 0);
364             ctx.setLineWidth(3);
365             ctx.setSourceRGB(1, 0, 0);
366             ctx.rectangle(0, 0, width, height);
367             ctx.stroke();
368         }
369     }
370 }
371 
372 /* A place to hold Widget objects. Since each window has a unique HWND,
373  * we can use this hash type to store references to Widgets and call
374  * their window processing methods.
375  */
376 Widget[HWND] WidgetHandles;
377 
378 /*
379  * All Widget windows have this window procedure registered via RegisterClass(),
380  * we use it to dispatch to the appropriate Widget window processing method.
381  *
382  * A similar technique is used in the DFL and DGUI libraries for all of its
383  * windows and widgets.
384  */
385 extern (Windows)
386 LRESULT winDispatch(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
387 {
388     auto widget = hwnd in WidgetHandles;
389 
390     if (widget !is null)
391     {
392         return widget.process(message, wParam, lParam);
393     }
394 
395     return DefWindowProc(hwnd, message, wParam, lParam);
396 }
397 
398 extern (Windows)
399 LRESULT mainWinProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
400 {
401     static PaintBuffer paintBuffer;
402     static int width, height;
403     static int TimerID = 16;
404 
405     // Note: this was copied from another example, most of these are not required here
406     static int cxChar, cxCaps, cyChar, cxClient, cyClient, iMaxWidth;
407     static int iDeltaPerLine, iAccumDelta;  // for mouse wheel logic
408     int i, x, y, iVertPos, iHorzPos, iPaintStart, iPaintEnd;
409     SCROLLINFO  si;
410     TEXTMETRIC tm;
411     ULONG ulScrollLines;  // for mouse wheel logic
412 
413     static HMENU widgetID = cast(HMENU)0;  // todo: each widget has its own HMENU ID
414 
415     void draw(StateContext ctx)
416     {
417         ctx.setSourceRGB(0.2, 0.2, 0.2);
418         ctx.paint();
419     }
420 
421     switch (message)
422     {
423         case WM_CREATE:
424             auto hdc = GetDC(hwnd);
425 
426             GetTextMetrics(hdc, &tm);
427             cxChar = tm.tmAveCharWidth;
428             cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2;
429             cyChar = tm.tmHeight + tm.tmExternalLeading;
430 
431             ReleaseDC(hwnd, hdc);
432 
433             // Save the width of the three columns
434             iMaxWidth = 40 * cxChar + 22 * cxCaps;
435 
436             auto hDesk = GetDesktopWindow();
437             RECT rc;
438             GetClientRect(hDesk, &rc);
439 
440             auto localHdc = GetDC(hwnd);
441             paintBuffer = new PaintBuffer(localHdc, rc.right, rc.bottom);
442 
443             auto hWindow = CreateWindow(WidgetClass.toUTF16z, null,
444                            WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN,  // WS_CLIPCHILDREN is necessary
445                            0, 0, 0, 0,
446                            hwnd, widgetID,                                        // child ID
447                            cast(HINSTANCE)GetWindowLongPtr(hwnd, GWL_HINSTANCE),  // hInstance
448                            null);
449 
450             auto widget = new TestWidget(hWindow, 400, 400);
451             WidgetHandles[hWindow] = widget;
452 
453             auto size = widget.size;
454             MoveWindow(hWindow, size.width / 3, size.width / 3, size.width, size.height, true);
455 
456             goto case;
457 
458         // Fall through for mouse wheel information
459         case WM_SETTINGCHANGE:
460         {
461             SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &ulScrollLines, 0);
462 
463             // ulScrollLines usually equals 3 or 0 (for no scrolling)
464             // WHEEL_DELTA equals 120, so iDeltaPerLine will be 40
465             if (ulScrollLines)
466                 iDeltaPerLine = WHEEL_DELTA / ulScrollLines;
467             else
468                 iDeltaPerLine = 0;
469 
470             return 0;
471         }
472 
473         case WM_LBUTTONDOWN:
474         {
475             SetFocus(hwnd);
476             return 0;
477         }
478 
479         case WM_SIZE:
480         {
481             width  = LOWORD(lParam);
482             height = HIWORD(lParam);
483 
484             enum windowLength = 100;
485 
486             // Set vertical scroll bar range and page size
487             si.fMask = SIF_RANGE | SIF_PAGE;
488             si.nMin  = 0;
489             si.nMax  = windowLength;
490             si.nPage = height / cyChar;
491             SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
492 
493             // Set horizontal scroll bar range and page size
494             si.fMask = SIF_RANGE | SIF_PAGE;
495             si.nMin  = 0;
496             si.nMax  = 2 + iMaxWidth / cxChar;
497             si.nPage = width / cxChar;
498             SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
499             return 0;
500         }
501 
502         case WM_VSCROLL:
503         {
504             // Get all the vertical scroll bar information
505             si.fMask = SIF_ALL;
506             GetScrollInfo(hwnd, SB_VERT, &si);
507 
508             // Save the position for comparison later on
509             iVertPos = si.nPos;
510 
511             switch (LOWORD(wParam))
512             {
513                 case SB_TOP:
514                     si.nPos = si.nMin;
515                     break;
516 
517                 case SB_BOTTOM:
518                     si.nPos = si.nMax;
519                     break;
520 
521                 case SB_LINEUP:
522                     si.nPos -= 1;
523                     break;
524 
525                 case SB_LINEDOWN:
526                     si.nPos += 1;
527                     break;
528 
529                 case SB_PAGEUP:
530                     si.nPos -= si.nPage;
531                     break;
532 
533                 case SB_PAGEDOWN:
534                     si.nPos += si.nPage;
535                     break;
536 
537                 case SB_THUMBTRACK:
538                     si.nPos = si.nTrackPos;
539                     break;
540 
541                 default:
542                     break;
543             }
544 
545             // Set the position and then retrieve it.  Due to adjustments
546             // by Windows it may not be the same as the value set.
547             si.fMask = SIF_POS;
548             SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
549             GetScrollInfo(hwnd, SB_VERT, &si);
550 
551             // If the position has changed, scroll the window and update it
552             if (si.nPos != iVertPos)
553             {
554                 auto rect = RECT(0, 0, width, 400);
555                 // second to last argument is area that is scrolled
556                 //~ ScrollWindow(hwnd, 0, cyChar * (iVertPos - si.nPos), null, &rect);  // last arg is boundary
557                 ScrollWindow(hwnd, 0, cyChar * (iVertPos - si.nPos), null, null);
558                 UpdateWindow(hwnd);
559             }
560 
561             return 0;
562         }
563 
564         case WM_HSCROLL:
565         {
566             // Get all the vertical scroll bar information
567             si.fMask = SIF_ALL;
568 
569             // Save the position for comparison later on
570             GetScrollInfo(hwnd, SB_HORZ, &si);
571             iHorzPos = si.nPos;
572 
573             switch (LOWORD(wParam))
574             {
575                 case SB_LINELEFT:
576                     si.nPos -= 1;
577                     break;
578 
579                 case SB_LINERIGHT:
580                     si.nPos += 1;
581                     break;
582 
583                 case SB_PAGELEFT:
584                     si.nPos -= si.nPage;
585                     break;
586 
587                 case SB_PAGERIGHT:
588                     si.nPos += si.nPage;
589                     break;
590 
591                 case SB_THUMBPOSITION:
592                     si.nPos = si.nTrackPos;
593                     break;
594 
595                 default:
596                     break;
597             }
598 
599             // Set the position and then retrieve it.  Due to adjustments
600             //   by Windows it may not be the same as the value set.
601             si.fMask = SIF_POS;
602             SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
603             GetScrollInfo(hwnd, SB_HORZ, &si);
604 
605             // If the position has changed, scroll the window
606             if (si.nPos != iHorzPos)
607             {
608                 ScrollWindow(hwnd, cxChar * (iHorzPos - si.nPos), 0, null, null);
609             }
610 
611             return 0;
612         }
613 
614         case WM_MOUSEWHEEL:
615         {
616             if (iDeltaPerLine == 0)  // no scroll
617                 break;
618 
619             iAccumDelta += cast(short) HIWORD(wParam);     // 120 or -120
620 
621             while (iAccumDelta >= iDeltaPerLine)
622             {
623                 SendMessage(hwnd, WM_VSCROLL, SB_LINEUP, 0);
624                 iAccumDelta -= iDeltaPerLine;
625             }
626 
627             while (iAccumDelta <= -iDeltaPerLine)
628             {
629                 SendMessage(hwnd, WM_VSCROLL, SB_LINEDOWN, 0);
630                 iAccumDelta += iDeltaPerLine;
631             }
632 
633             return 0;
634         }
635 
636         case WM_PAINT:
637         {
638             auto ctx = paintBuffer.ctx;
639             auto hBuffer = paintBuffer.hBuffer;
640             PAINTSTRUCT ps;
641             auto hdc = BeginPaint(hwnd, &ps);
642             auto boundRect = ps.rcPaint;
643 
644             draw(StateContext(paintBuffer.ctx));
645 
646             with (boundRect)
647             {
648                 BitBlt(hdc, left, top, right - left, bottom - top, paintBuffer.hBuffer, left, top, SRCCOPY);
649             }
650 
651             EndPaint(hwnd, &ps);
652             return 0;
653         }
654 
655         case WM_TIMER:
656         {
657             InvalidateRect(hwnd, null, true);
658             return 0;
659         }
660 
661         case WM_DESTROY:
662         {
663             paintBuffer.clear();
664             PostQuitMessage(0);
665             return 0;
666         }
667 
668         default:
669     }
670 
671     return DefWindowProc(hwnd, message, wParam, lParam);
672 }
673 
674 string WidgetClass = "WidgetClass";
675 
676 int myWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow)
677 {
678     string appName = "layered drawing";
679 
680     HWND hwnd;
681     MSG  msg;
682     WNDCLASS wndclass;
683 
684     /* One class for the main window */
685     wndclass.lpfnWndProc = &mainWinProc;
686     wndclass.cbClsExtra  = 0;
687     wndclass.cbWndExtra  = 0;
688     wndclass.hInstance   = hInstance;
689     wndclass.hIcon       = LoadIcon(null, IDI_APPLICATION);
690     wndclass.hCursor     = LoadCursor(null, IDC_ARROW);
691     wndclass.hbrBackground = null;
692     wndclass.lpszMenuName  = null;
693     wndclass.lpszClassName = appName.toUTF16z;
694 
695     if (!RegisterClass(&wndclass))
696     {
697         MessageBox(null, "This program requires Windows NT!", appName.toUTF16z, MB_ICONERROR);
698         return 0;
699     }
700 
701     /* Separate window class for Widgets. */
702     wndclass.hbrBackground = null;
703     wndclass.lpfnWndProc   = &winDispatch;
704     wndclass.cbWndExtra    = 0;
705     wndclass.hIcon         = null;
706     wndclass.lpszClassName = WidgetClass.toUTF16z;
707 
708     if (!RegisterClass(&wndclass))
709     {
710         MessageBox(null, "This program requires Windows NT!", appName.toUTF16z, MB_ICONERROR);
711         return 0;
712     }
713 
714     hwnd = CreateWindow(appName.toUTF16z, "layered drawing",
715                         WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,  // WS_CLIPCHILDREN is necessary
716                         CW_USEDEFAULT, CW_USEDEFAULT,
717                         CW_USEDEFAULT, CW_USEDEFAULT,
718                         null, null, hInstance, null);
719 
720     ShowWindow(hwnd, iCmdShow);
721     UpdateWindow(hwnd);
722 
723     while (GetMessage(&msg, null, 0, 0))
724     {
725         TranslateMessage(&msg);
726         DispatchMessage(&msg);
727     }
728 
729     return msg.wParam;
730 }
731 
732 extern (Windows)
733 int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow)
734 {
735     int result;
736 
737 
738     try
739     {
740         Runtime.initialize();
741         myWinMain(hInstance, hPrevInstance, lpCmdLine, iCmdShow);
742         Runtime.terminate();
743     }
744     catch (Throwable o)
745     {
746         MessageBox(null, o.toString().toUTF16z, "Error", MB_OK | MB_ICONEXCLAMATION);
747         result = -1;
748     }
749 
750     return result;
751 }