1 /*
2   Copyright Chris Jones 2020.
3   Distributed under the Boost Software License, Version 1.0.
4   See accompanying file Licence.txt or copy at...
5   https://www.boost.org/LICENSE_1_0.txt
6 */
7 
8 module window;
9 
10 import std.stdio;
11 import core.sys.windows.windows;
12 import std..string;
13 import std.conv;
14 import dg2d;
15 
16 import dg2d.roundrect : dg2dRoundRect = RoundRect; // need as RoundRect is in wingdi too.
17 
18 static import gdi = core.sys.windows.wingdi;
19 
20 pragma(lib, "gdi32");
21 pragma(lib, "user32");
22 
23 /*
24   Windows Message Loop
25 */
26 
27 int WindowsMessageLoop()
28 {    
29     MSG  msg;
30     while (GetMessageA(&msg, null, 0, 0))
31     {
32         TranslateMessage(&msg);
33         DispatchMessageA(&msg);
34     }
35     return cast(int) msg.wParam;
36 }
37 
38 /*
39   Window Proc, not sure what to do about catching Errors/Exceptions, seems to
40   just hang no matter what, tried assert(0), ExitProcess etc.. just doesnt close
41 */
42 
43 extern(Windows)
44 LRESULT WindowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) nothrow
45 {
46     try
47     {
48         auto window = cast(Window) (cast(void*) GetWindowLongPtr(hwnd, GWLP_USERDATA));
49 
50         if (window is null)
51             return DefWindowProcA(hwnd, msg, wparam, lparam);
52         else
53             return window.windowProc(hwnd, msg, wparam, lparam);
54     }
55     catch (Exception e)
56     {
57         try { writeln(e.toString()); }
58         catch(Exception what) {}
59         PostQuitMessage(0);
60         return 0;
61     }
62 }
63 
64 /*
65   Window class, bare bones WinAPI wrapper
66 */
67 
68 class Window
69 {
70 private:
71 
72     HWND        m_handle;
73     DWORD       m_style;
74     DWORD       m_exstyle;
75     string      m_title;
76     int         m_width;
77     int         m_height;
78     Canvas      m_canvas;
79 
80     Widget      m_client;
81 
82     static immutable char[] wndclass  = "GFXWindow";
83     static HINSTANCE hinstance;
84 
85     UINT_PTR m_timer;
86 
87     static this()
88     {       
89         hinstance = HINSTANCE(GetModuleHandleA(NULL));
90 
91         WNDCLASSEXA wcx;
92         wcx.cbSize          = wcx.sizeof;
93         wcx.style			= CS_DBLCLKS;
94         wcx.lpfnWndProc	    = &WindowProc;
95         wcx.cbClsExtra		= 0;
96         wcx.cbWndExtra		= (void*).sizeof;
97         wcx.hInstance		= hinstance;
98         wcx.hIcon			= NULL;
99         wcx.hCursor		    = LoadCursor(NULL, IDC_ARROW);
100         wcx.hbrBackground	= NULL;
101         wcx.lpszMenuName	= NULL;
102         wcx.lpszClassName	= wndclass.ptr;
103         wcx.hIconSm		    = NULL;
104 
105         RegisterClassExA(&wcx);
106     }
107 
108 public:
109 
110     void createWindow(int x, int y, int w, int h, string title)
111     {
112         if (m_handle) destroyWindow();
113 
114         m_width = w;
115         m_height = h;
116 
117         m_style = WS_OVERLAPPEDWINDOW;
118         m_exstyle = WS_EX_APPWINDOW;
119 
120         RECT rect;
121         rect.left = x;
122         rect.top = y;
123         rect.right = x+w;
124         rect.bottom = y+h;
125 
126         AdjustWindowRectEx(&rect, m_style, false, m_exstyle);
127 
128         m_handle = CreateWindowExA(
129             WS_EX_APPWINDOW, wndclass.ptr,  toStringz(title), WS_OVERLAPPEDWINDOW,
130             rect.left, rect.top, rect.right-rect.left, rect.bottom-rect.top,
131             null, null, hinstance, NULL
132             );
133 
134         if (m_handle)
135         {
136             SetWindowLongPtrA(m_handle, GWLP_USERDATA, cast(LONG_PTR)( cast(void*)this ));
137             ShowWindow(m_handle, SW_SHOW);
138 
139             m_timer = SetTimer(m_handle, 0, 1000/20, NULL);
140         }
141         else
142         {
143             writeln("oops... coud not create window");
144             PostQuitMessage(0);
145         }
146     }
147 
148     void destroyWindow()
149     {
150         if (m_handle)
151         {
152             DestroyWindow(m_handle);
153             m_handle = null;
154         }
155     }
156 
157     this()
158     {
159     }
160 
161     ~this()
162     {
163         destroyWindow();
164     }
165 
166     void setBounds(int x, int y, int w, int h)
167     {
168         assert((w >= 0) && (h >= 0));
169 
170         m_width = w;
171         m_height = h;
172 
173         if (m_handle)
174         {
175             RECT r = RECT(x, y, x+w, y+h);
176             AdjustWindowRectEx(&r, m_style, false, m_exstyle);
177             MoveWindow(m_handle, r.left, r.top, r.right-r.left,
178                 r.bottom-r.top, true);
179         }
180     }
181 
182     bool isVisible()
183     {
184         if (m_handle) return (IsWindowVisible(m_handle) != 0);
185         return false;
186     }
187 
188     void repaint()
189     {
190         InvalidateRect(m_handle,null,0);
191     }
192 
193     void repaint(int x0, int y0, int x1, int y1)
194     {
195         RECT rect;
196         rect.left = x0;
197         rect.top = y0;
198         rect.right = x1;
199         rect.bottom = y1;
200         InvalidateRect(m_handle,&rect,0);
201     }
202 
203     // Window proc handler
204 
205     LRESULT windowProc(HWND hwnd, UINT msg, WPARAM _wparam, LPARAM _lparam)
206     {
207         WPARAM wparam = _wparam;
208         LPARAM lparam = _lparam;
209 
210         switch (msg)
211         {
212             case(WM_PAINT):              wm_Paint(); break;
213             case(WM_CLOSE):              wm_Close(); break;
214             case(WM_DESTROY):            wm_Destroy(); break;
215             case(WM_LBUTTONDOWN):        wm_Mouse(MouseEvent.LeftDown, cast(uint)wparam, cast(uint)lparam); break;
216             case(WM_LBUTTONDBLCLK):      wm_Mouse(MouseEvent.LeftDblCk, cast(uint)wparam, cast(uint)lparam); break;
217             case(WM_RBUTTONDOWN):        wm_Mouse(MouseEvent.RightDown, cast(uint)wparam, cast(uint)lparam); break;
218             case(WM_RBUTTONDBLCLK):      wm_Mouse(MouseEvent.RightDblCk, cast(uint)wparam, cast(uint)lparam); break;
219             case(WM_MOUSEMOVE):          wm_Mouse(MouseEvent.Move, cast(uint)wparam, cast(uint)lparam); break;
220             case(WM_TIMER):              wm_Timer(); break;
221 
222             default: return DefWindowProc(hwnd, msg, wparam, lparam); // can't take m_handle there because of WM_CREATE
223         }
224         return 0;
225     }
226 
227     void wm_Paint()
228     { 
229         PAINTSTRUCT ps;
230         BeginPaint(m_handle, &ps);
231         int l = ps.rcPaint.left;
232         int t = ps.rcPaint.top;
233         int r = ps.rcPaint.right;
234         int b = ps.rcPaint.bottom;
235 
236         if (m_canvas is null) m_canvas = new Canvas(r, b);
237         
238         if ((m_canvas.width < r) || (m_canvas.height < b))
239         {
240             m_canvas.resize(r, b);
241         }
242 
243         m_canvas.resetView();
244         m_canvas.setClip(l,t,r,b);
245 
246         onPaint(m_canvas);
247         if (m_client !is null) m_client.internalPaint(m_canvas);
248 
249         BITMAPINFO info;
250         info.bmiHeader.biSize          = info.sizeof;
251         info.bmiHeader.biWidth         = m_canvas.stride;
252         info.bmiHeader.biHeight        = -m_canvas.height;
253         info.bmiHeader.biPlanes        = 1;
254         info.bmiHeader.biBitCount      = 32;
255         info.bmiHeader.biCompression   = BI_RGB;
256         info.bmiHeader.biSizeImage     = m_canvas.stride*m_canvas.height*4;
257         info.bmiHeader.biXPelsPerMeter = 0;
258         info.bmiHeader.biYPelsPerMeter = 0;
259         info.bmiHeader.biClrUsed       = 0;
260         info.bmiHeader.biClrImportant  = 0;
261 
262         SetDIBitsToDevice(
263             ps.hdc, 0, 0, m_canvas.stride, m_canvas.height,0, 0, 0,
264             m_canvas.height, m_canvas.pixels, &info, DIB_RGB_COLORS);
265 
266         EndPaint(m_handle, &ps);
267     }
268 
269     void wm_Mouse(MouseEvent evt, uint wparam, uint lparam)
270     {
271         if (m_client !is null)
272         {
273             MouseMsg msg;
274             msg.event = evt;
275             msg.left = ((wparam & MK_LBUTTON) != 0);
276             msg.middle = ((wparam & MK_MBUTTON) != 0);
277             msg.right = ((wparam & MK_RBUTTON) != 0);
278             msg.x = cast(short)(lparam); // couldnt find GET_X_PARAM etc...
279             msg.y = cast(short)(lparam>>16);
280             m_client.internalMouse(msg);
281         }
282     }
283 
284     void wm_Close()
285     {
286         PostQuitMessage(0);        
287     }
288 
289     void wm_Destroy()
290     {
291         m_handle = null;
292     }
293 
294     void wm_Timer()
295     {
296         if (m_client !is null) m_client.internalTimer();
297     }
298 
299     void onPaint(Canvas canvas)
300     {
301 
302     }
303 
304     void addClient(Widget widget)
305     {
306         if (widget.m_parent !is null) widget.m_parent.removeChild(widget);
307         widget.m_window = this;
308         m_client = widget;
309     }
310 
311     void removeClient(Widget widget)
312     {
313         if  (widget is m_client)
314         {
315             widget.m_window = null;
316             m_client = null;
317         }
318     }
319 
320 }
321 
322 // mouse stuff
323 
324 enum MouseEvent
325 {
326     LeftDown, MiddleDown, RightDown,
327     LeftUp, MiddleUp, RightUp,
328     LeftDblCk, MiddleDblCk, RightDblCk,
329     Move, Enter, Exit, EndFocus,
330     LeftDrag, RightDrag,
331     Wheel
332 }
333 
334 struct MouseMsg
335 {
336     MouseEvent  event;
337     int         x,y,w;
338     bool        focused;
339     bool        left;
340     bool        middle;
341     bool        right;
342     bool        shift;
343     bool        ctrl;
344     bool        alt;
345 }
346 
347 // Widget
348 
349 class Widget
350 {
351     this(int x, int y, int width, int height)
352     {
353         m_x = x;
354         m_y = y;
355         m_width = width;
356         m_height = height;
357     }
358 
359     void addChild(Widget widget)
360     {
361         if (widget.m_parent !is null) widget.m_parent.removeChild(widget);
362         if (widget.m_window !is null) widget.m_window.removeClient(widget);
363         m_widgets ~= widget;
364         widget.m_parent = this;
365     }
366 
367     void removeChild(Widget widget)
368     {
369         foreach(i, child; m_widgets)
370         {
371             if (child is widget)
372             {
373                 m_widgets[i..$-1] = m_widgets[i+1..$];
374                 m_widgets.length = m_widgets.length-1;
375                 widget.m_parent = null;
376                 return;
377             }
378         }   
379     }
380 
381     void repaint()
382     {
383         repaint(0,0,m_width,m_height);
384     }
385 
386     void repaint(int x0, int y0, int x1, int y1)
387     {
388         if (m_parent !is null)
389         {
390             m_parent.repaint(m_x+x0,m_y+y0,m_x+x1,m_y+y1);
391         }
392         else if (m_window !is null)
393         {
394             m_window.repaint(m_x+x0,m_y+y0,m_x+x1,m_y+y1);
395         }
396     }
397 
398     void onPaint(Canvas canvas)
399     {
400     }
401 
402     void onMouse(MouseMsg msg)
403     {
404     }
405 
406     void onTimer()
407     {
408     }
409 
410     int right()
411     {
412         return m_x+m_width;
413     }
414     
415     int bottom()
416     {
417         return m_y+m_height;
418     }
419 
420     bool contains(int x, int y)
421     {
422         return ((x >= m_x) && (x < m_x+m_width)
423             && (y >= m_y) && (y < m_y+m_height));
424     }
425 
426 private:
427 
428     void internalPaint(Canvas canvas)
429     {
430         onPaint(canvas);
431         auto state = canvas.getViewState();
432  
433         foreach(widget; m_widgets)
434         {
435             canvas.setView(state, widget.m_x, widget.m_y, widget.right, widget.bottom); 
436             if (!canvas.isClipEmpty) widget.internalPaint(canvas);
437         }
438 
439         canvas.resetState(state);
440     }
441    
442     // returns the widget that got the message
443 
444     Widget internalMouse(MouseMsg msg)
445     {
446         foreach_reverse(widget; m_widgets)
447         {
448             if (widget.contains(msg.x,msg.y))
449             {
450                 msg.x -= widget.m_x;
451                 msg.y -= widget.m_y;
452                 return widget.internalMouse(msg);
453             }
454         }
455         onMouse(msg);
456         return this;
457     }
458 
459     void internalTimer()
460     {
461         foreach_reverse(widget; m_widgets)
462         {
463             widget.onTimer();
464         }
465         onTimer();
466     }
467 
468     Widget m_parent;
469     Window m_window;
470     Widget[] m_widgets;
471 
472     int m_x,m_y,m_width,m_height;
473 }
474 
475 /*
476   Button class
477 */
478 
479 alias ButtonClick = void delegate();
480 
481 class Button : Widget
482 {
483     this(int x,int y, int w, int h, string text, Font f)
484     {
485         super(x,y,w,h);
486         m_text = text;
487         m_font = f;
488     }
489 
490     void setOnClick(ButtonClick onclick)
491     {
492         m_onclick = onclick;
493     }
494 
495     bool hitTest(int x, int y)
496     {
497         return ((x >= m_x) && (x < m_x+m_width)
498             && (y >= m_y) && (y < m_y+m_height));
499     }
500 
501     override void onPaint(Canvas c)
502     {
503         c.draw(dg2dRoundRect(0,0,m_width,m_height,10).asPath, 0x80a0c0ff, WindingRule.NonZero);
504         c.draw(dg2dRoundRect(2,2,m_width-2,m_height-2,10).asPath, 0xFF000000, WindingRule.NonZero);
505 
506         int tx = 20;
507         int ty = m_height - cast(int) (m_height - m_font.height) / 2;
508         c.drawText(tx,ty,m_text,m_font,0xFFffffff);
509     }
510 
511     override void onMouse(MouseMsg msg)
512     {
513         if ((msg.event == MouseEvent.LeftDown) ||
514             (msg.event == MouseEvent.LeftDblCk))
515         {
516             if (m_onclick !is null) m_onclick();
517         }
518     }
519 
520     void setText(string txt)
521     {
522         m_text = txt;
523     }
524 
525 private:
526 
527     string m_text;
528     Font m_font;
529     ButtonClick m_onclick;
530 }
531 
532 class Label : Widget
533 {
534     this(int x,int y, int w, int h, string text, Font f)
535     {
536         super(x,y,w,h);
537         m_text = text;
538         m_font = f;
539     }
540 
541     override void onPaint(Canvas c)
542     {
543         c.draw( dg2dRoundRect(0,0,m_width,m_height,4).asPath, 0x80000000, WindingRule.NonZero);
544         c.draw( dg2dRoundRect(1,1,m_width-1,m_height-1,4).asPath, 0xFFFFFFFF, WindingRule.NonZero);
545         int tx = cast(int) (m_width - m_font.getStrWidth(m_text)) / 2;
546         int ty = m_height - cast(int) (m_height - m_font.height) / 2;
547         c.drawText(tx,ty,m_text,m_font,0xFF000000);
548     }
549 
550     void setText(string text)
551     {
552         m_text = text;
553     }
554 
555 private:
556 
557     string m_text;
558     Font m_font;
559 }