chiark / gitweb /
Added a status bar.
[sgt-puzzles.git] / windows.c
1 /*
2  * windows.c: Windows front end for my puzzle collection.
3  */
4
5 #include <windows.h>
6 #include <commctrl.h>
7
8 #include <stdio.h>
9 #include <assert.h>
10 #include <stdarg.h>
11 #include <stdlib.h>
12 #include <time.h>
13
14 #include "puzzles.h"
15
16 #define IDM_NEW       0x0010
17 #define IDM_RESTART   0x0020
18 #define IDM_UNDO      0x0030
19 #define IDM_REDO      0x0040
20 #define IDM_QUIT      0x0050
21 #define IDM_PRESETS   0x0100
22
23 #ifdef DEBUG
24 static FILE *debug_fp = NULL;
25 static HANDLE debug_hdl = INVALID_HANDLE_VALUE;
26 static int debug_got_console = 0;
27
28 void dputs(char *buf)
29 {
30     DWORD dw;
31
32     if (!debug_got_console) {
33         if (AllocConsole()) {
34             debug_got_console = 1;
35             debug_hdl = GetStdHandle(STD_OUTPUT_HANDLE);
36         }
37     }
38     if (!debug_fp) {
39         debug_fp = fopen("debug.log", "w");
40     }
41
42     if (debug_hdl != INVALID_HANDLE_VALUE) {
43         WriteFile(debug_hdl, buf, strlen(buf), &dw, NULL);
44     }
45     fputs(buf, debug_fp);
46     fflush(debug_fp);
47 }
48
49 void debug_printf(char *fmt, ...)
50 {
51     char buf[4096];
52     va_list ap;
53
54     va_start(ap, fmt);
55     vsprintf(buf, fmt, ap);
56     dputs(buf);
57     va_end(ap);
58 }
59
60 #define debug(x) (debug_printf x)
61
62 #else
63
64 #define debug(x)
65
66 #endif
67
68 struct font {
69     HFONT font;
70     int type;
71     int size;
72 };
73
74 struct frontend {
75     midend_data *me;
76     HWND hwnd, statusbar;
77     HBITMAP bitmap, prevbm;
78     HDC hdc_bm;
79     COLORREF *colours;
80     HBRUSH *brushes;
81     HPEN *pens;
82     HRGN clip;
83     UINT timer;
84     int npresets;
85     game_params **presets;
86     struct font *fonts;
87     int nfonts, fontsize;
88 };
89
90 void fatal(char *fmt, ...)
91 {
92     char buf[2048];
93     va_list ap;
94
95     va_start(ap, fmt);
96     vsprintf(buf, fmt, ap);
97     va_end(ap);
98
99     MessageBox(NULL, buf, "Fatal error", MB_ICONEXCLAMATION | MB_OK);
100
101     exit(1);
102 }
103
104 void status_bar(frontend *fe, char *text)
105 {
106     SetWindowText(fe->statusbar, text);
107 }
108
109 void frontend_default_colour(frontend *fe, float *output)
110 {
111     DWORD c = GetSysColor(COLOR_MENU); /* ick */
112
113     output[0] = (float)(GetRValue(c) / 255.0);
114     output[1] = (float)(GetGValue(c) / 255.0);
115     output[2] = (float)(GetBValue(c) / 255.0);
116 }
117
118 void clip(frontend *fe, int x, int y, int w, int h)
119 {
120     if (!fe->clip) {
121         fe->clip = CreateRectRgn(0, 0, 1, 1);
122         GetClipRgn(fe->hdc_bm, fe->clip);
123     }
124
125     IntersectClipRect(fe->hdc_bm, x, y, x+w, y+h);
126 }
127
128 void unclip(frontend *fe)
129 {
130     assert(fe->clip);
131     SelectClipRgn(fe->hdc_bm, fe->clip);
132 }
133
134 void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize,
135                int align, int colour, char *text)
136 {
137     int i;
138
139     /*
140      * Find or create the font.
141      */
142     for (i = 0; i < fe->nfonts; i++)
143         if (fe->fonts[i].type == fonttype && fe->fonts[i].size == fontsize)
144             break;
145
146     if (i == fe->nfonts) {
147         if (fe->fontsize <= fe->nfonts) {
148             fe->fontsize = fe->nfonts + 10;
149             fe->fonts = sresize(fe->fonts, fe->fontsize, struct font);
150         }
151
152         fe->nfonts++;
153
154         fe->fonts[i].type = fonttype;
155         fe->fonts[i].size = fontsize;
156
157         /*
158          * FIXME: Really I should make at least _some_ effort to
159          * pick the correct font.
160          */
161         fe->fonts[i].font = CreateFont(-fontsize, 0, 0, 0, 0,
162                                        FALSE, FALSE, FALSE, DEFAULT_CHARSET,
163                                        OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
164                                        DEFAULT_QUALITY,
165                                        (fonttype == FONT_FIXED ?
166                                         FIXED_PITCH | FF_DONTCARE :
167                                         VARIABLE_PITCH | FF_SWISS),
168                                        NULL);
169     }
170
171     /*
172      * Position and draw the text.
173      */
174     {
175         HFONT oldfont;
176         TEXTMETRIC tm;
177         SIZE size;
178
179         oldfont = SelectObject(fe->hdc_bm, fe->fonts[i].font);
180         if (GetTextMetrics(fe->hdc_bm, &tm)) {
181             if (align & ALIGN_VCENTRE)
182                 y -= (tm.tmAscent+tm.tmDescent)/2;
183             else
184                 y -= tm.tmAscent;
185         }
186         if (GetTextExtentPoint32(fe->hdc_bm, text, strlen(text), &size)) {
187             if (align & ALIGN_HCENTRE)
188                 x -= size.cx / 2;
189             else if (align & ALIGN_HRIGHT)
190                 x -= size.cx;
191         }
192         SetBkMode(fe->hdc_bm, TRANSPARENT);
193         TextOut(fe->hdc_bm, x, y, text, strlen(text));
194         SelectObject(fe->hdc_bm, oldfont);
195     }
196 }
197
198 void draw_rect(frontend *fe, int x, int y, int w, int h, int colour)
199 {
200     if (w == 1 && h == 1) {
201         /*
202          * Rectangle() appears to get uppity if asked to draw a 1x1
203          * rectangle, presumably on the grounds that that's beneath
204          * its dignity and you ought to be using SetPixel instead.
205          * So I will.
206          */
207         SetPixel(fe->hdc_bm, x, y, fe->colours[colour]);
208     } else {
209         HBRUSH oldbrush = SelectObject(fe->hdc_bm, fe->brushes[colour]);
210         HPEN oldpen = SelectObject(fe->hdc_bm, fe->pens[colour]);
211         Rectangle(fe->hdc_bm, x, y, x+w, y+h);
212         SelectObject(fe->hdc_bm, oldbrush);
213         SelectObject(fe->hdc_bm, oldpen);
214     }
215 }
216
217 void draw_line(frontend *fe, int x1, int y1, int x2, int y2, int colour)
218 {
219     HPEN oldpen = SelectObject(fe->hdc_bm, fe->pens[colour]);
220     MoveToEx(fe->hdc_bm, x1, y1, NULL);
221     LineTo(fe->hdc_bm, x2, y2);
222     SetPixel(fe->hdc_bm, x2, y2, fe->colours[colour]);
223     SelectObject(fe->hdc_bm, oldpen);
224 }
225
226 void draw_polygon(frontend *fe, int *coords, int npoints,
227                   int fill, int colour)
228 {
229     POINT *pts = snewn(npoints+1, POINT);
230     int i;
231
232     for (i = 0; i <= npoints; i++) {
233         int j = (i < npoints ? i : 0);
234         pts[i].x = coords[j*2];
235         pts[i].y = coords[j*2+1];
236     }
237
238     if (fill) {
239         HBRUSH oldbrush = SelectObject(fe->hdc_bm, fe->brushes[colour]);
240         HPEN oldpen = SelectObject(fe->hdc_bm, fe->pens[colour]);
241         Polygon(fe->hdc_bm, pts, npoints);
242         SelectObject(fe->hdc_bm, oldbrush);
243         SelectObject(fe->hdc_bm, oldpen);
244     } else {
245         HPEN oldpen = SelectObject(fe->hdc_bm, fe->pens[colour]);
246         Polyline(fe->hdc_bm, pts, npoints+1);
247         SelectObject(fe->hdc_bm, oldpen);
248     }
249
250     sfree(pts);
251 }
252
253 void start_draw(frontend *fe)
254 {
255     HDC hdc_win;
256     hdc_win = GetDC(fe->hwnd);
257     fe->hdc_bm = CreateCompatibleDC(hdc_win);
258     fe->prevbm = SelectObject(fe->hdc_bm, fe->bitmap);
259     ReleaseDC(fe->hwnd, hdc_win);
260     fe->clip = NULL;
261 }
262
263 void draw_update(frontend *fe, int x, int y, int w, int h)
264 {
265     RECT r;
266
267     r.left = x;
268     r.top = y;
269     r.right = x + w;
270     r.bottom = y + h;
271
272     InvalidateRect(fe->hwnd, &r, FALSE);
273 }
274
275 void end_draw(frontend *fe)
276 {
277     SelectObject(fe->hdc_bm, fe->prevbm);
278     DeleteDC(fe->hdc_bm);
279     if (fe->clip) {
280         DeleteObject(fe->clip);
281         fe->clip = NULL;
282     }
283 }
284
285 void deactivate_timer(frontend *fe)
286 {
287     KillTimer(fe->hwnd, fe->timer);
288     fe->timer = 0;
289 }
290
291 void activate_timer(frontend *fe)
292 {
293     fe->timer = SetTimer(fe->hwnd, fe->timer, 20, NULL);
294 }
295
296 static frontend *new_window(HINSTANCE inst)
297 {
298     frontend *fe;
299     int x, y;
300     RECT r, sr;
301     HDC hdc;
302
303     fe = snew(frontend);
304     fe->me = midend_new(fe);
305     midend_new_game(fe->me, NULL);
306     midend_size(fe->me, &x, &y);
307
308     fe->timer = 0;
309
310     {
311         int i, ncolours;
312         float *colours;
313
314         colours = midend_colours(fe->me, &ncolours);
315
316         fe->colours = snewn(ncolours, COLORREF);
317         fe->brushes = snewn(ncolours, HBRUSH);
318         fe->pens = snewn(ncolours, HPEN);
319
320         for (i = 0; i < ncolours; i++) {
321             fe->colours[i] = RGB(255 * colours[i*3+0],
322                                  255 * colours[i*3+1],
323                                  255 * colours[i*3+2]);
324             fe->brushes[i] = CreateSolidBrush(fe->colours[i]);
325             if (!fe->brushes[i])
326                 MessageBox(fe->hwnd, "ooh", "eck", MB_OK);
327             fe->pens[i] = CreatePen(PS_SOLID, 1, fe->colours[i]);
328         }
329     }
330
331     r.left = r.top = 0;
332     r.right = x;
333     r.bottom = y;
334     AdjustWindowRectEx(&r, WS_OVERLAPPEDWINDOW &~
335                        (WS_THICKFRAME | WS_MAXIMIZEBOX | WS_OVERLAPPED),
336                        TRUE, 0);
337
338     fe->hwnd = CreateWindowEx(0, game_name, game_name,
339                               WS_OVERLAPPEDWINDOW &~
340                               (WS_THICKFRAME | WS_MAXIMIZEBOX),
341                               CW_USEDEFAULT, CW_USEDEFAULT,
342                               r.right - r.left, r.bottom - r.top,
343                               NULL, NULL, inst, NULL);
344
345     {
346         HMENU bar = CreateMenu();
347         HMENU menu = CreateMenu();
348
349         AppendMenu(bar, MF_ENABLED|MF_POPUP, (UINT)menu, "Game");
350         AppendMenu(menu, MF_ENABLED, IDM_NEW, "New");
351         AppendMenu(menu, MF_ENABLED, IDM_RESTART, "Restart");
352
353         if ((fe->npresets = midend_num_presets(fe->me)) > 0) {
354             HMENU sub = CreateMenu();
355             int i;
356
357             AppendMenu(menu, MF_ENABLED|MF_POPUP, (UINT)sub, "Type");
358
359             fe->presets = snewn(fe->npresets, game_params *);
360
361             for (i = 0; i < fe->npresets; i++) {
362                 char *name;
363
364                 midend_fetch_preset(fe->me, i, &name, &fe->presets[i]);
365
366                 /*
367                  * FIXME: we ought to go through and do something
368                  * with ampersands here.
369                  */
370
371                 AppendMenu(sub, MF_ENABLED, IDM_PRESETS + 0x10 * i, name);
372             }
373         }
374
375         AppendMenu(menu, MF_SEPARATOR, 0, 0);
376         AppendMenu(menu, MF_ENABLED, IDM_UNDO, "Undo");
377         AppendMenu(menu, MF_ENABLED, IDM_REDO, "Redo");
378         AppendMenu(menu, MF_SEPARATOR, 0, 0);
379         AppendMenu(menu, MF_ENABLED, IDM_QUIT, "Exit");
380         SetMenu(fe->hwnd, bar);
381     }
382
383     if (midend_wants_statusbar(fe->me)) {
384         fe->statusbar = CreateWindowEx(0, STATUSCLASSNAME, "ooh",
385                                        WS_CHILD | WS_VISIBLE,
386                                        0, 0, 0, 0, /* status bar does these */
387                                        fe->hwnd, NULL, inst, NULL);
388         GetWindowRect(fe->statusbar, &sr);
389         SetWindowPos(fe->hwnd, NULL, 0, 0,
390                      r.right - r.left, r.bottom - r.top + sr.bottom - sr.top,
391                      SWP_NOMOVE | SWP_NOZORDER);
392         SetWindowPos(fe->statusbar, NULL, 0, y, x, sr.bottom - sr.top,
393                      SWP_NOZORDER);
394     } else {
395         fe->statusbar = NULL;
396     }
397
398     hdc = GetDC(fe->hwnd);
399     fe->bitmap = CreateCompatibleBitmap(hdc, x, y);
400     ReleaseDC(fe->hwnd, hdc);
401
402     SetWindowLong(fe->hwnd, GWL_USERDATA, (LONG)fe);
403
404     ShowWindow(fe->hwnd, SW_NORMAL);
405     SetForegroundWindow(fe->hwnd);
406
407     midend_redraw(fe->me);
408
409     return fe;
410 }
411
412 static LRESULT CALLBACK WndProc(HWND hwnd, UINT message,
413                                 WPARAM wParam, LPARAM lParam)
414 {
415     frontend *fe = (frontend *)GetWindowLong(hwnd, GWL_USERDATA);
416
417     switch (message) {
418       case WM_CLOSE:
419         DestroyWindow(hwnd);
420         return 0;
421       case WM_COMMAND:
422         switch (wParam & ~0xF) {       /* low 4 bits reserved to Windows */
423           case IDM_NEW:
424             if (!midend_process_key(fe->me, 0, 0, 'n'))
425                 PostQuitMessage(0);
426             break;
427           case IDM_RESTART:
428             if (!midend_process_key(fe->me, 0, 0, 'r'))
429                 PostQuitMessage(0);
430             break;
431           case IDM_UNDO:
432             if (!midend_process_key(fe->me, 0, 0, 'u'))
433                 PostQuitMessage(0);
434             break;
435           case IDM_REDO:
436             if (!midend_process_key(fe->me, 0, 0, '\x12'))
437                 PostQuitMessage(0);
438             break;
439           case IDM_QUIT:
440             if (!midend_process_key(fe->me, 0, 0, 'q'))
441                 PostQuitMessage(0);
442             break;
443           default:
444             {
445                 int p = ((wParam &~ 0xF) - IDM_PRESETS) / 0x10;
446
447                 if (p >= 0 && p < fe->npresets) {
448                     RECT r, sr;
449                     HDC hdc;
450                     int x, y;
451
452                     midend_set_params(fe->me, fe->presets[p]);
453                     midend_new_game(fe->me, NULL);
454                     midend_size(fe->me, &x, &y);
455
456                     r.left = r.top = 0;
457                     r.right = x;
458                     r.bottom = y;
459                     AdjustWindowRectEx(&r, WS_OVERLAPPEDWINDOW &~
460                                        (WS_THICKFRAME | WS_MAXIMIZEBOX |
461                                         WS_OVERLAPPED),
462                                        TRUE, 0);
463
464                     if (fe->statusbar != NULL) {
465                         GetWindowRect(fe->statusbar, &sr);
466                     } else {
467                         sr.left = sr.right = sr.top = sr.bottom = 0;
468                     }
469                     SetWindowPos(fe->hwnd, NULL, 0, 0,
470                                  r.right - r.left,
471                                  r.bottom - r.top + sr.bottom - sr.top,
472                                  SWP_NOMOVE | SWP_NOZORDER);
473                     if (fe->statusbar != NULL)
474                         SetWindowPos(fe->statusbar, NULL, 0, y, x,
475                                      sr.bottom - sr.top, SWP_NOZORDER);
476
477                     DeleteObject(fe->bitmap);
478
479                     hdc = GetDC(fe->hwnd);
480                     fe->bitmap = CreateCompatibleBitmap(hdc, x, y);
481                     ReleaseDC(fe->hwnd, hdc);
482
483                     midend_redraw(fe->me);
484                 }
485             }
486             break;
487         }
488         break;
489       case WM_DESTROY:
490         PostQuitMessage(0);
491         return 0;
492       case WM_PAINT:
493         {
494             PAINTSTRUCT p;
495             HDC hdc, hdc2;
496             HBITMAP prevbm;
497
498             hdc = BeginPaint(hwnd, &p);
499             hdc2 = CreateCompatibleDC(hdc);
500             prevbm = SelectObject(hdc2, fe->bitmap);
501             BitBlt(hdc,
502                    p.rcPaint.left, p.rcPaint.top,
503                    p.rcPaint.right - p.rcPaint.left,
504                    p.rcPaint.bottom - p.rcPaint.top,
505                    hdc2,
506                    p.rcPaint.left, p.rcPaint.top,
507                    SRCCOPY);
508             SelectObject(hdc2, prevbm);
509             DeleteDC(hdc2);
510             EndPaint(hwnd, &p);
511         }
512         return 0;
513       case WM_KEYDOWN:
514         {
515             int key = -1;
516
517             switch (wParam) {
518               case VK_LEFT: key = CURSOR_LEFT; break;
519               case VK_RIGHT: key = CURSOR_RIGHT; break;
520               case VK_UP: key = CURSOR_UP; break;
521               case VK_DOWN: key = CURSOR_DOWN; break;
522                 /*
523                  * Diagonal keys on the numeric keypad.
524                  */
525               case VK_PRIOR:
526                 if (!(lParam & 0x01000000)) key = CURSOR_UP_RIGHT;
527                 break;
528               case VK_NEXT:
529                 if (!(lParam & 0x01000000)) key = CURSOR_DOWN_RIGHT;
530                 break;
531               case VK_HOME:
532                 if (!(lParam & 0x01000000)) key = CURSOR_UP_LEFT;
533                 break;
534               case VK_END:
535                 if (!(lParam & 0x01000000)) key = CURSOR_DOWN_LEFT;
536                 break;
537                 /*
538                  * Numeric keypad keys with Num Lock on.
539                  */
540               case VK_NUMPAD4: key = CURSOR_LEFT; break;
541               case VK_NUMPAD6: key = CURSOR_RIGHT; break;
542               case VK_NUMPAD8: key = CURSOR_UP; break;
543               case VK_NUMPAD2: key = CURSOR_DOWN; break;
544               case VK_NUMPAD9: key = CURSOR_UP_RIGHT; break;
545               case VK_NUMPAD3: key = CURSOR_DOWN_RIGHT; break;
546               case VK_NUMPAD7: key = CURSOR_UP_LEFT; break;
547               case VK_NUMPAD1: key = CURSOR_DOWN_LEFT; break;
548             }
549
550             if (key != -1) {
551                 if (!midend_process_key(fe->me, 0, 0, key))
552                     PostQuitMessage(0);
553             } else {
554                 MSG m;
555                 m.hwnd = hwnd;
556                 m.message = WM_KEYDOWN;
557                 m.wParam = wParam;
558                 m.lParam = lParam & 0xdfff;
559                 TranslateMessage(&m);
560             }
561         }
562         break;
563       case WM_LBUTTONDOWN:
564       case WM_RBUTTONDOWN:
565       case WM_MBUTTONDOWN:
566         {
567             int button;
568
569             /*
570              * Shift-clicks count as middle-clicks, since otherwise
571              * two-button Windows users won't have any kind of
572              * middle click to use.
573              */
574             if (message == WM_MBUTTONDOWN || (wParam & MK_SHIFT))
575                 button = MIDDLE_BUTTON;
576             else if (message == WM_LBUTTONDOWN)
577                 button = LEFT_BUTTON;
578             else
579                 button = RIGHT_BUTTON;
580                 
581             if (!midend_process_key(fe->me, LOWORD(lParam),
582                                     HIWORD(lParam), button))
583                 PostQuitMessage(0);
584         }
585         break;
586       case WM_CHAR:
587         if (!midend_process_key(fe->me, 0, 0, (unsigned char)wParam))
588             PostQuitMessage(0);
589         return 0;
590       case WM_TIMER:
591         if (fe->timer)
592             midend_timer(fe->me, (float)0.02);
593         return 0;
594     }
595
596     return DefWindowProc(hwnd, message, wParam, lParam);
597 }
598
599 int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmdline, int show)
600 {
601     MSG msg;
602
603     srand(time(NULL));
604
605     InitCommonControls();
606
607     if (!prev) {
608         WNDCLASS wndclass;
609
610         wndclass.style = 0;
611         wndclass.lpfnWndProc = WndProc;
612         wndclass.cbClsExtra = 0;
613         wndclass.cbWndExtra = 0;
614         wndclass.hInstance = inst;
615         wndclass.hIcon = LoadIcon(inst, IDI_APPLICATION);
616         wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
617         wndclass.hbrBackground = NULL;
618         wndclass.lpszMenuName = NULL;
619         wndclass.lpszClassName = game_name;
620
621         RegisterClass(&wndclass);
622     }
623
624     new_window(inst);
625
626     while (GetMessage(&msg, NULL, 0, 0)) {
627         DispatchMessage(&msg);
628     }
629
630     return msg.wParam;
631 }