Added a status bar.
[sgt/puzzles] / 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 }