1 module window_framework;
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  + Shows one way to implement Widgets on top of WinAPI's
12  + windowing framework. Makes use of Johannes Phauf's
13  + updated std.signals module (renamed to signals.d to
14  + avoid clashes).
15  +
16  + Demonstrates simple MenuBar and Menu widgets, as well
17  + as mouse-enter and mouse-leave behavior.
18  +/
19 
20 /*
21  * Major todo: We need to decouple assigning parents from construction,
22  * otherwise we can't reparent or new() a widget and append
23  * it to a container widget. So maybe only appending would create
24  * the backbuffer.
25  *
26  * Major todo: Fork the Widget project and separate initialization
27  * routines.
28  */
29 
30 import core.memory;
31 import core.runtime;
32 import core.thread;
33 import core.stdc.config;
34 
35 import std.algorithm;
36 import std.array;
37 import std.conv;
38 import std.exception;
39 import std.functional;
40 import std.math;
41 import std.random;
42 import std.range;
43 import std.stdio;
44 // import std.signals;  // outdated
45 import util.signals;  // new
46 import std.string;
47 import std.traits;
48 import std.utf;
49 
50 pragma(lib, "gdi32.lib");
51 
52 import windows.windef;
53 import windows.winuser;
54 import windows.wingdi;
55 
56 alias std.algorithm.min min;
57 alias std.algorithm.max max;
58 
59 import cairo.c.cairo;
60 import cairo.cairo;
61 import cairo.win32;
62 
63 import util.rounded_rectangle;
64 
65 struct StateContext
66 {
67     // maybe add a scale() call, althought that wouldn't work good on specific shapes.
68     Context ctx;
69 
70     this(Context ctx)
71     {
72         this.ctx = ctx;
73         ctx.save();
74     }
75 
76     ~this()
77     {
78         ctx.restore();
79     }
80 
81     alias ctx this;
82 }
83 
84 class PaintBuffer
85 {
86     /* Each window with paintbuffer consumes 3 GDI objects (hBuffer, hBitmap, and hdc of the window). */
87     this(HDC localHdc, int cxClient, int cyClient)
88     {
89         width  = cxClient;
90         height = cyClient;
91 
92         hBuffer     = CreateCompatibleDC(localHdc);
93         hBitmap     = CreateCompatibleBitmap(localHdc, cxClient, cyClient);
94         hOldBitmap  = SelectObject(hBuffer, hBitmap);
95 
96         surf        = new Win32Surface(hBuffer);
97         ctx         = Context(surf);
98         initialized = true;
99     }
100 
101     ~this()
102     {
103         if (initialized)
104         {
105             initialized = false;
106             clear();
107         }
108     }
109 
110     void clear()
111     {
112         //~ surf.dispose();
113         //~ ctx.dispose();
114         //~ surf.finish();  // @BUG@: runtime exceptions
115 
116         SelectObject(hBuffer, hOldBitmap);
117         DeleteObject(hBitmap);
118         DeleteDC(hBuffer);
119 
120         initialized = false;
121     }
122 
123     bool initialized;
124     int  width, height;
125     HDC  hBuffer;
126     HBITMAP hBitmap;
127     HBITMAP hOldBitmap;
128     Context ctx;
129     Surface surf;
130 }
131 
132 HANDLE makeWindow(HWND hwnd, int childID = 1, string classname = WidgetClass, string description = null)
133 {
134     return CreateWindow(classname.toUTF16z, description.toUTF16z,
135                         WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN,        // WS_CLIPCHILDREN is necessary
136                         0, 0, 0, 0,                                           // Size!int and Position are set by MoveWindow
137                         hwnd, cast(HANDLE)childID,                            // child ID
138                         cast(HINSTANCE)GetWindowLongPtr(hwnd, GWL_HINSTANCE), // hInstance
139                         null);
140 }
141 
142 abstract class Widget
143 {
144     TRACKMOUSEEVENT mouseTrack;
145     Widget parent;
146     HWND hwnd;
147     PaintBuffer paintBuffer;
148     PAINTSTRUCT ps;
149 
150     int xOffset;
151     int yOffset;
152     int width, height;
153 
154     bool needsRedraw = true;
155     bool isHidden;
156     bool mouseEntered;
157 
158     Signal!() MouseLDown;
159     alias MouseLDown MouseClick;
160 
161     Signal!(int, int) MouseMove;
162     Signal!() MouseEnter;
163     Signal!() MouseLeave;
164 
165     this(Widget parent, int xOffset = 0, int yOffset = 0, int width = 0, int height = 0)
166     {
167         this.parent  = parent;
168         this.xOffset = xOffset;
169         this.yOffset = yOffset;
170         this.width   = width;
171         this.height  = height;
172 
173         assert(parent !is null);
174         hwnd = makeWindow(parent.hwnd);
175         WidgetHandles[hwnd] = this;
176 
177         this.hwnd = hwnd;
178         MoveWindow(hwnd, 0, 0, size.width, size.height, true);
179 
180         mouseTrack = TRACKMOUSEEVENT(TRACKMOUSEEVENT.sizeof, TME_LEAVE, hwnd, 0);
181     }
182 
183     // children of the main window use this as the main window isn't a Widget type yet (to be done)
184     this(HWND hParentWindow, int xOffset = 0, int yOffset = 0, int width = 0, int height = 0)
185     {
186         this.parent  = null;
187         this.xOffset = xOffset;
188         this.yOffset = yOffset;
189         this.width   = width;
190         this.height  = height;
191 
192         hwnd = makeWindow(hParentWindow);
193         WidgetHandles[hwnd] = this;
194 
195         this.hwnd = hwnd;
196         MoveWindow(hwnd, 0, 0, size.width, size.height, true);
197     }
198 
199     @property Size!int size()
200     {
201         return Size!int(width, height);
202     }
203 
204     @property void size(Size!int newsize)
205     {
206         width  = newsize.width;
207         height = newsize.height;
208 
209         auto localHdc = GetDC(hwnd);
210 
211         if (paintBuffer !is null)
212         {
213             paintBuffer.clear();
214         }
215 
216         paintBuffer = new PaintBuffer(localHdc, width, height);
217         ReleaseDC(hwnd, localHdc);
218 
219         MoveWindow(hwnd, xOffset, yOffset, width, height, true);
220 
221         needsRedraw = true;
222         blit();
223     }
224 
225     void moveTo(int newXOffset, int newYOffset)
226     {
227         xOffset = newXOffset;
228         yOffset = newYOffset;
229         MoveWindow(hwnd, xOffset, yOffset, width, height, true);
230     }
231 
232     void show()
233     {
234         if (isHidden)
235         {
236             ShowWindow(hwnd, true);
237             isHidden = false;
238         }
239     }
240 
241     void hide()
242     {
243         if (!isHidden)
244         {
245             ShowWindow(hwnd, false);
246             isHidden = true;
247         }
248     }
249 
250     LRESULT process(UINT message, WPARAM wParam, LPARAM lParam)
251     {
252         switch (message)
253         {
254             case WM_ERASEBKGND:
255             {
256                 return 1;
257             }
258 
259             case WM_PAINT:
260             {
261                 OnPaint(hwnd, message, wParam, lParam);
262                 return 0;
263             }
264 
265             case WM_SIZE:
266             {
267                 width  = LOWORD(lParam);
268                 height = HIWORD(lParam);
269 
270                 size(Size!int(width, height));
271                 return 0;
272             }
273 
274             case WM_LBUTTONDOWN:
275             {
276                 MouseLDown.emit();
277                 return 0;
278             }
279 
280             case WM_MOUSELEAVE:
281             {
282                 MouseLeave.emit();
283                 mouseEntered = false;
284                 return 0;
285             }
286 
287             case WM_MOUSEMOVE:
288             {
289                 TrackMouseEvent(&mouseTrack);
290 
291                 // @BUG@ WinAPI bug, calling ShowWindow in succession can create
292                 // an infinite loop due to an odd WM_MOUSEMOVE call to the window
293                 // which issued the ShowWindow call to other windows.
294                 static LPARAM oldPosition;
295                 if (lParam != oldPosition)
296                 {
297                     if (!mouseEntered)
298                     {
299                         MouseEnter.emit();
300                         mouseEntered = true;
301                     }
302 
303                     oldPosition = lParam;
304                     auto xMousePos = cast(short)LOWORD(lParam);
305                     auto yMousePos = cast(short)HIWORD(lParam);
306 
307                     MouseMove.emit(xMousePos, yMousePos);
308                 }
309 
310                 return 0;
311             }
312 
313             //~ case WM_TIMER:
314             //~ {
315                 //~ blit();
316                 //~ return 0;
317             //~ }
318 
319             case WM_DESTROY:
320             {
321                 // @BUG@
322                 // Not doing this here causes exceptions being thrown from within cairo
323                 // when calling surface.dispose(). I'm not sure why yet.
324                 paintBuffer.clear();
325                 return 0;
326             }
327 
328             default:
329         }
330 
331         return DefWindowProc(hwnd, message, wParam, lParam);
332     }
333 
334     void redraw()
335     {
336         needsRedraw = true;
337         blit();
338     }
339 
340     void blit()
341     {
342         InvalidateRect(hwnd, null, true);
343     }
344 
345     void OnPaint(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
346     {
347         auto ctx       = &paintBuffer.ctx;
348         auto hBuffer   = paintBuffer.hBuffer;
349         auto hdc       = BeginPaint(hwnd, &ps);
350         auto boundRect = ps.rcPaint;
351 
352         if (needsRedraw)
353         {
354             draw(StateContext(*ctx));
355             needsRedraw = false;
356         }
357 
358         with (boundRect)
359         {
360             BitBlt(hdc, left, top, right - left, bottom - top, hBuffer, left, top, SRCCOPY);
361         }
362 
363         EndPaint(hwnd, &ps);
364     }
365 
366     void draw(StateContext ctx) { }
367 }
368 
369 enum Alignment
370 {
371     left,
372     right,
373     center,
374     top,
375     bottom,
376 }
377 
378 class Button : Widget
379 {
380     string name;
381     string fontName;
382     int fontSize;
383     Alignment alignment;
384     bool selected = true;
385 
386     this(Widget parent, string name, string fontName, int fontSize,
387          Alignment alignment = Alignment.center, int xOffset = 0, int yOffset = 0, int width = 0, int height = 0)
388     {
389         auto textWidth = name.length * fontSize;
390         width = textWidth;
391         height = fontSize * 2;
392         this.name = name;
393         this.fontName = fontName;
394         this.fontSize = fontSize;
395         this.alignment = alignment;
396 
397         super(parent, xOffset, yOffset, width, height);
398     }
399 
400     override void draw(StateContext ctx)
401     {
402         ctx.setSourceRGB(1, 1, 0);
403         ctx.paint();
404 
405         ctx.setSourceRGB(0, 0, 0);
406         ctx.selectFontFace(fontName, FontSlant.CAIRO_FONT_SLANT_NORMAL, FontWeight.CAIRO_FONT_WEIGHT_NORMAL);
407         ctx.setFontSize(fontSize);
408 
409         final switch (alignment)
410         {
411             case Alignment.left:
412             {
413                 ctx.moveTo(0, fontSize);
414                 break;
415             }
416 
417             case Alignment.right:
418             {
419                 break;
420             }
421 
422             case Alignment.center:
423             {
424                 // todo
425                 auto centerPos = (width - (name.length * 6)) / 2;
426                 //~ ctx.moveTo(centerPos, fontSize);
427                 ctx.moveTo(0, fontSize);
428                 break;
429             }
430 
431             case Alignment.top:
432             {
433                 break;
434             }
435 
436             case Alignment.bottom:
437             {
438                 break;
439             }
440         }
441 
442         ctx.showText(name);
443     }
444 }
445 
446 class MenuItem : Widget
447 {
448     string name;
449     string fontName;
450     int fontSize;
451     bool selected;
452 
453     this(Widget parent, string name, string fontName, int fontSize,
454          int xOffset = 0, int yOffset = 0, int width = 0, int height = 0)
455     {
456         auto textWidth = name.length * fontSize;
457         width = textWidth;
458 
459         height = fontSize * 2;
460         this.name = name;
461         this.fontName = fontName;
462         this.fontSize = fontSize;
463 
464         super(parent, xOffset, yOffset, width, height);
465 
466         this.MouseEnter.connect({ selected = true; redraw(); });
467         this.MouseLeave.connect({ selected = false; redraw(); });
468     }
469 
470     override void draw(StateContext ctx)
471     {
472         if (selected)
473             ctx.setSourceRGB(1, 0.9, 0);
474         else
475             ctx.setSourceRGB(1, 1, 0);
476 
477         ctx.paint();
478 
479         ctx.setSourceRGB(0, 0, 0);
480         ctx.selectFontFace(fontName, FontSlant.CAIRO_FONT_SLANT_NORMAL, FontWeight.CAIRO_FONT_WEIGHT_NORMAL);
481         ctx.setFontSize(fontSize);
482         ctx.moveTo(0, fontSize);
483         ctx.showText(name);
484     }
485 }
486 
487 class Menu : Widget
488 {
489     MenuItem[] menuItems;
490     size_t lastYOffset;
491 
492     // todo: main window will have to be a widget
493     //~ this(Widget parent, MenuType menuType, int xOffset = 0, int yOffset = 0, int width = 0, int height = 0)
494     //~ {
495         //~ super(parent, xOffset, yOffset, width, height);
496         //~ this.menuType = menuType;
497     //~ }
498 
499     this(HWND hParentWindow, int xOffset = 0, int yOffset = 0, int width = 0, int height = 0)
500     {
501         super(hParentWindow, xOffset, yOffset, width, height);
502     }
503 
504     void append(MenuItem menuItem)
505     {
506         width = max(width, menuItem.name.length * menuItem.fontSize);
507         menuItems ~= menuItem;
508         menuItem.moveTo(0, lastYOffset);
509 
510         this.size = Size!int(width, menuItems.length * menuItem.height);
511         lastYOffset += menuItem.height;
512     }
513 
514     MenuItem append(string name, string fontName, int fontSize)
515     {
516         auto textWidth = name.length * fontSize;
517         width = max(width, textWidth);
518 
519         auto menuItem = new MenuItem(this, name, fontName, fontSize, 0);
520         menuItem.moveTo(0, lastYOffset);
521         menuItems ~= menuItem;
522 
523         this.size = Size!int(width, menuItems.length * menuItem.height);
524         lastYOffset += menuItem.height;
525 
526         return menuItem;
527     }
528 
529     override void draw(StateContext ctx)
530     {
531         // todo: draw bg and separators
532         ctx.setSourceRGB(0.8, 0.8, 0.8);
533         ctx.paint();
534     }
535 }
536 
537 class MenuBar : Widget
538 {
539     Menu[] menus;
540     Button[] buttons;
541     size_t lastXOffset;
542     Menu activeMenu;
543     bool isMenuOpened;
544 
545     // todo: main window will have to be a widget
546     //~ this(Widget parent, int xOffset = 0, int yOffset = 0, int width = 0, int height = 0)
547     //~ {
548         //~ super(parent, xOffset, yOffset, width, height);
549     //~ }
550 
551     this(HWND hParentWindow, int xOffset = 0, int yOffset = 0, int width = 0, int height = 0)
552     {
553         super(hParentWindow, xOffset, yOffset, width, height);
554     }
555 
556     void showMenu(size_t index)
557     {
558         assert(index < menus.length);
559 
560         if (activeMenu !is null)
561             activeMenu.hide();
562 
563         activeMenu = menus[index];
564 
565         if (isMenuOpened)
566             activeMenu.show();
567         else
568             activeMenu.hide();
569     }
570 
571     void append(Menu menu, string name)
572     {
573         static size_t menuIndex;
574         enum fontSize = 10;
575         enum fontName = "Arial";
576         immutable yOffset = 2 * fontSize;
577 
578         auto button = new Button(this, name, fontName, fontSize);
579         this.size = Size!int(width + button.width, yOffset);
580 
581         int frameIndex = menuIndex++;
582 
583         buttons ~= button;
584         button.moveTo(lastXOffset, 0);
585 
586         button.MouseLDown.connect({ this.isMenuOpened ^= 1; this.showMenu(frameIndex); });
587         button.MouseEnter.connect({ this.showMenu(frameIndex); });
588 
589         menus ~= menu;
590         menu.hide();
591         menu.moveTo(lastXOffset, yOffset);
592 
593         lastXOffset += button.width;
594     }
595 
596     override void draw(StateContext ctx)
597     {
598         ctx.setSourceRGB(0, 1, 1);
599         ctx.paint();
600     }
601 }
602 
603 /* A place to hold Widget objects. Since each window has a unique HWND,
604  * we can use this hash type to store references to Widgets and call
605  * their window processing methods.
606  */
607 Widget[HWND] WidgetHandles;
608 
609 /*
610  * All Widget windows have this window procedure registered via RegisterClass(),
611  * we use it to dispatch to the appropriate Widget window processing method.
612  *
613  * A similar technique is used in the DFL and DGUI libraries for all of its
614  * windows and widgets.
615  */
616 extern (Windows)
617 LRESULT winDispatch(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
618 {
619     auto widget = hwnd in WidgetHandles;
620 
621     if (widget !is null)
622     {
623         return widget.process(message, wParam, lParam);
624     }
625 
626     return DefWindowProc(hwnd, message, wParam, lParam);
627 }
628 
629 extern (Windows)
630 LRESULT mainWinProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
631 {
632     static PaintBuffer paintBuffer;
633     static int width, height;
634 
635     void draw(StateContext ctx)
636     {
637         ctx.setSourceRGB(1, 1, 1);
638         ctx.paint();
639     }
640 
641     switch (message)
642     {
643         case WM_CREATE:
644         {
645             auto hDesk = GetDesktopWindow();
646             RECT rc;
647             GetClientRect(hDesk, &rc);
648 
649             auto localHdc = GetDC(hwnd);
650             paintBuffer = new PaintBuffer(localHdc, rc.right, rc.bottom);
651 
652             auto menuBar = new MenuBar(hwnd);
653 
654             // todo
655             //~ auto fontSettings = FontSettings("Arial", 10);
656 
657             auto fileMenu = new Menu(hwnd);
658 
659             auto item = fileMenu.append("item1", "Arial", 10);
660             item.MouseLDown.connect( { writeln("Clicked."); } );
661 
662             menuBar.append(fileMenu, "File");
663 
664             return 0;
665         }
666 
667         case WM_LBUTTONDOWN:
668         {
669             SetFocus(hwnd);
670             return 0;
671         }
672 
673         case WM_SIZE:
674         {
675             width  = LOWORD(lParam);
676             height = HIWORD(lParam);
677             return 0;
678         }
679 
680         case WM_PAINT:
681         {
682             auto ctx     = paintBuffer.ctx;
683             auto hBuffer = paintBuffer.hBuffer;
684             PAINTSTRUCT ps;
685             auto hdc       = BeginPaint(hwnd, &ps);
686             auto boundRect = ps.rcPaint;
687 
688             draw(StateContext(paintBuffer.ctx));
689 
690             with (boundRect)
691             {
692                 BitBlt(hdc, left, top, right - left, bottom - top, paintBuffer.hBuffer, left, top, SRCCOPY);
693             }
694 
695             EndPaint(hwnd, &ps);
696             return 0;
697         }
698 
699         case WM_TIMER:
700         {
701             InvalidateRect(hwnd, null, true);
702             return 0;
703         }
704 
705         case WM_MOUSEWHEEL:
706         {
707             return 0;
708         }
709 
710         case WM_DESTROY:
711         {
712             PostQuitMessage(0);
713             return 0;
714         }
715 
716         default:
717     }
718 
719     return DefWindowProc(hwnd, message, wParam, lParam);
720 }
721 
722 string WidgetClass = "WidgetClass";
723 
724 int myWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow)
725 {
726     string appName = "Window Framework";
727 
728     HWND hwnd;
729     MSG  msg;
730     WNDCLASS wndclass;
731 
732     /* One class for the main window */
733     wndclass.lpfnWndProc   = &mainWinProc;
734     wndclass.cbClsExtra    = 0;
735     wndclass.cbWndExtra    = 0;
736     wndclass.hInstance     = hInstance;
737     wndclass.hIcon         = LoadIcon(null, IDI_APPLICATION);
738     wndclass.hCursor       = LoadCursor(null, IDC_ARROW);
739     wndclass.hbrBackground = null;
740     wndclass.lpszMenuName  = null;
741     wndclass.lpszClassName = appName.toUTF16z;
742 
743     if (!RegisterClass(&wndclass))
744     {
745         MessageBox(null, "This program requires Windows NT!", appName.toUTF16z, MB_ICONERROR);
746         return 0;
747     }
748 
749     /* Separate window class for Widgets. */
750     wndclass.hbrBackground = null;
751     wndclass.lpfnWndProc   = &winDispatch;
752     wndclass.cbWndExtra    = 0;
753     wndclass.hIcon         = null;
754     wndclass.lpszClassName = WidgetClass.toUTF16z;
755 
756     if (!RegisterClass(&wndclass))
757     {
758         MessageBox(null, "This program requires Windows NT!", appName.toUTF16z, MB_ICONERROR);
759         return 0;
760     }
761 
762     hwnd = CreateWindow(appName.toUTF16z, "step sequencer",
763                         WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,  // WS_CLIPCHILDREN is necessary
764                         cast(int)(1680 / 3.3), 1050 / 3,
765                         400, 400,
766                         null, null, hInstance, null);
767 
768     ShowWindow(hwnd, iCmdShow);
769     UpdateWindow(hwnd);
770 
771     while (GetMessage(&msg, null, 0, 0))
772     {
773         TranslateMessage(&msg);
774         DispatchMessage(&msg);
775     }
776 
777     return msg.wParam;
778 }
779 
780 extern (Windows)
781 int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow)
782 {
783     int result;
784 
785 
786     try
787     {
788         Runtime.initialize();
789         myWinMain(hInstance, hPrevInstance, lpCmdLine, iCmdShow);
790         Runtime.terminate();
791     }
792     catch (Throwable o)
793     {
794         MessageBox(null, o.toString().toUTF16z, "Error", MB_OK | MB_ICONEXCLAMATION);
795         result = -1;
796     }
797 
798     return result;
799 }