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