chiark / gitweb /
Implemented text and clipping primitives in the frontend, and added
[sgt-puzzles.git] / windows.c
index 9365c14fc70ae7bba31ab013e9b06c4f1e77d2a9..ea5d7a27393c1c4cc1bd5378d89a152d52bf3976 100644 (file)
--- a/windows.c
+++ b/windows.c
@@ -1,3 +1,599 @@
 /*
  * windows.c: Windows front end for my puzzle collection.
  */
+
+#include <windows.h>
+
+#include <stdio.h>
+#include <assert.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <time.h>
+
+#include "puzzles.h"
+
+#define IDM_NEW       0x0010
+#define IDM_RESTART   0x0020
+#define IDM_UNDO      0x0030
+#define IDM_REDO      0x0040
+#define IDM_QUIT      0x0050
+#define IDM_PRESETS   0x0100
+
+#ifdef DEBUG
+static FILE *debug_fp = NULL;
+static HANDLE debug_hdl = INVALID_HANDLE_VALUE;
+static int debug_got_console = 0;
+
+void dputs(char *buf)
+{
+    DWORD dw;
+
+    if (!debug_got_console) {
+       if (AllocConsole()) {
+           debug_got_console = 1;
+           debug_hdl = GetStdHandle(STD_OUTPUT_HANDLE);
+       }
+    }
+    if (!debug_fp) {
+       debug_fp = fopen("debug.log", "w");
+    }
+
+    if (debug_hdl != INVALID_HANDLE_VALUE) {
+       WriteFile(debug_hdl, buf, strlen(buf), &dw, NULL);
+    }
+    fputs(buf, debug_fp);
+    fflush(debug_fp);
+}
+
+void debug_printf(char *fmt, ...)
+{
+    char buf[4096];
+    va_list ap;
+
+    va_start(ap, fmt);
+    vsprintf(buf, fmt, ap);
+    dputs(buf);
+    va_end(ap);
+}
+
+#define debug(x) (debug_printf x)
+
+#else
+
+#define debug(x)
+
+#endif
+
+struct font {
+    HFONT font;
+    int type;
+    int size;
+};
+
+struct frontend {
+    midend_data *me;
+    HWND hwnd;
+    HBITMAP bitmap, prevbm;
+    HDC hdc_bm;
+    COLORREF *colours;
+    HBRUSH *brushes;
+    HPEN *pens;
+    HRGN clip;
+    UINT timer;
+    int npresets;
+    game_params **presets;
+    struct font *fonts;
+    int nfonts, fontsize;
+};
+
+void fatal(char *fmt, ...)
+{
+    char buf[2048];
+    va_list ap;
+
+    va_start(ap, fmt);
+    vsprintf(buf, fmt, ap);
+    va_end(ap);
+
+    MessageBox(NULL, buf, "Fatal error", MB_ICONEXCLAMATION | MB_OK);
+
+    exit(1);
+}
+
+void frontend_default_colour(frontend *fe, float *output)
+{
+    DWORD c = GetSysColor(COLOR_MENU); /* ick */
+
+    output[0] = (float)(GetRValue(c) / 255.0);
+    output[1] = (float)(GetGValue(c) / 255.0);
+    output[2] = (float)(GetBValue(c) / 255.0);
+}
+
+void clip(frontend *fe, int x, int y, int w, int h)
+{
+    if (!fe->clip) {
+       fe->clip = CreateRectRgn(0, 0, 1, 1);
+       GetClipRgn(fe->hdc_bm, fe->clip);
+    }
+
+    IntersectClipRect(fe->hdc_bm, x, y, x+w, y+h);
+}
+
+void unclip(frontend *fe)
+{
+    assert(fe->clip);
+    SelectClipRgn(fe->hdc_bm, fe->clip);
+}
+
+void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize,
+               int align, int colour, char *text)
+{
+    int i;
+
+    /*
+     * Find or create the font.
+     */
+    for (i = 0; i < fe->nfonts; i++)
+        if (fe->fonts[i].type == fonttype && fe->fonts[i].size == fontsize)
+            break;
+
+    if (i == fe->nfonts) {
+        if (fe->fontsize <= fe->nfonts) {
+            fe->fontsize = fe->nfonts + 10;
+            fe->fonts = sresize(fe->fonts, fe->fontsize, struct font);
+        }
+
+        fe->nfonts++;
+
+        fe->fonts[i].type = fonttype;
+        fe->fonts[i].size = fontsize;
+
+        /*
+         * FIXME: Really I should make at least _some_ effort to
+         * pick the correct font.
+         */
+        fe->fonts[i].font = CreateFont(-fontsize, 0, 0, 0, 0,
+                                      FALSE, FALSE, FALSE, DEFAULT_CHARSET,
+                                      OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
+                                      DEFAULT_QUALITY,
+                                      (fonttype == FONT_FIXED ?
+                                       FIXED_PITCH | FF_DONTCARE :
+                                       VARIABLE_PITCH | FF_SWISS),
+                                      NULL);
+    }
+
+    /*
+     * Position and draw the text.
+     */
+    {
+       HFONT oldfont;
+       TEXTMETRIC tm;
+       SIZE size;
+
+       oldfont = SelectObject(fe->hdc_bm, fe->fonts[i].font);
+       if (GetTextMetrics(fe->hdc_bm, &tm)) {
+           if (align & ALIGN_VCENTRE)
+               y -= (tm.tmAscent+tm.tmDescent)/2;
+           else
+               y -= tm.tmAscent;
+       }
+       if (GetTextExtentPoint32(fe->hdc_bm, text, strlen(text), &size)) {
+           if (align & ALIGN_HCENTRE)
+               x -= size.cx / 2;
+           else if (align & ALIGN_HRIGHT)
+               x -= size.cx;
+       }
+       SetBkMode(fe->hdc_bm, TRANSPARENT);
+       TextOut(fe->hdc_bm, x, y, text, strlen(text));
+       SelectObject(fe->hdc_bm, oldfont);
+    }
+}
+
+void draw_rect(frontend *fe, int x, int y, int w, int h, int colour)
+{
+    if (w == 1 && h == 1) {
+       /*
+        * Rectangle() appears to get uppity if asked to draw a 1x1
+        * rectangle, presumably on the grounds that that's beneath
+        * its dignity and you ought to be using SetPixel instead.
+        * So I will.
+        */
+       SetPixel(fe->hdc_bm, x, y, fe->colours[colour]);
+    } else {
+       HBRUSH oldbrush = SelectObject(fe->hdc_bm, fe->brushes[colour]);
+       HPEN oldpen = SelectObject(fe->hdc_bm, fe->pens[colour]);
+       Rectangle(fe->hdc_bm, x, y, x+w, y+h);
+       SelectObject(fe->hdc_bm, oldbrush);
+       SelectObject(fe->hdc_bm, oldpen);
+    }
+}
+
+void draw_line(frontend *fe, int x1, int y1, int x2, int y2, int colour)
+{
+    HPEN oldpen = SelectObject(fe->hdc_bm, fe->pens[colour]);
+    MoveToEx(fe->hdc_bm, x1, y1, NULL);
+    LineTo(fe->hdc_bm, x2, y2);
+    SetPixel(fe->hdc_bm, x2, y2, fe->colours[colour]);
+    SelectObject(fe->hdc_bm, oldpen);
+}
+
+void draw_polygon(frontend *fe, int *coords, int npoints,
+                  int fill, int colour)
+{
+    POINT *pts = snewn(npoints+1, POINT);
+    int i;
+
+    for (i = 0; i <= npoints; i++) {
+       int j = (i < npoints ? i : 0);
+       pts[i].x = coords[j*2];
+       pts[i].y = coords[j*2+1];
+    }
+
+    if (fill) {
+       HBRUSH oldbrush = SelectObject(fe->hdc_bm, fe->brushes[colour]);
+       HPEN oldpen = SelectObject(fe->hdc_bm, fe->pens[colour]);
+       Polygon(fe->hdc_bm, pts, npoints);
+       SelectObject(fe->hdc_bm, oldbrush);
+       SelectObject(fe->hdc_bm, oldpen);
+    } else {
+       HPEN oldpen = SelectObject(fe->hdc_bm, fe->pens[colour]);
+       Polyline(fe->hdc_bm, pts, npoints+1);
+       SelectObject(fe->hdc_bm, oldpen);
+    }
+
+    sfree(pts);
+}
+
+void start_draw(frontend *fe)
+{
+    HDC hdc_win;
+    hdc_win = GetDC(fe->hwnd);
+    fe->hdc_bm = CreateCompatibleDC(hdc_win);
+    fe->prevbm = SelectObject(fe->hdc_bm, fe->bitmap);
+    ReleaseDC(fe->hwnd, hdc_win);
+    fe->clip = NULL;
+}
+
+void draw_update(frontend *fe, int x, int y, int w, int h)
+{
+    RECT r;
+
+    r.left = x;
+    r.top = y;
+    r.right = x + w;
+    r.bottom = y + h;
+
+    InvalidateRect(fe->hwnd, &r, FALSE);
+}
+
+void end_draw(frontend *fe)
+{
+    SelectObject(fe->hdc_bm, fe->prevbm);
+    DeleteDC(fe->hdc_bm);
+    if (fe->clip) {
+       DeleteObject(fe->clip);
+       fe->clip = NULL;
+    }
+}
+
+void deactivate_timer(frontend *fe)
+{
+    KillTimer(fe->hwnd, fe->timer);
+    fe->timer = 0;
+}
+
+void activate_timer(frontend *fe)
+{
+    fe->timer = SetTimer(fe->hwnd, fe->timer, 20, NULL);
+}
+
+static frontend *new_window(HINSTANCE inst)
+{
+    frontend *fe;
+    int x, y;
+    RECT r;
+    HDC hdc;
+
+    fe = snew(frontend);
+    fe->me = midend_new(fe);
+    midend_new_game(fe->me, NULL);
+    midend_size(fe->me, &x, &y);
+
+    fe->timer = 0;
+
+    {
+       int i, ncolours;
+        float *colours;
+
+        colours = midend_colours(fe->me, &ncolours);
+
+       fe->colours = snewn(ncolours, COLORREF);
+       fe->brushes = snewn(ncolours, HBRUSH);
+       fe->pens = snewn(ncolours, HPEN);
+
+       for (i = 0; i < ncolours; i++) {
+           fe->colours[i] = RGB(255 * colours[i*3+0],
+                                255 * colours[i*3+1],
+                                255 * colours[i*3+2]);
+           fe->brushes[i] = CreateSolidBrush(fe->colours[i]);
+           if (!fe->brushes[i])
+               MessageBox(fe->hwnd, "ooh", "eck", MB_OK);
+           fe->pens[i] = CreatePen(PS_SOLID, 1, fe->colours[i]);
+       }
+    }
+
+    r.left = r.top = 0;
+    r.right = x;
+    r.bottom = y;
+    AdjustWindowRectEx(&r, WS_OVERLAPPEDWINDOW &~
+                      (WS_THICKFRAME | WS_MAXIMIZEBOX | WS_OVERLAPPED),
+                      TRUE, 0);
+
+    fe->hwnd = CreateWindowEx(0, game_name, game_name,
+                             WS_OVERLAPPEDWINDOW &~
+                             (WS_THICKFRAME | WS_MAXIMIZEBOX),
+                             CW_USEDEFAULT, CW_USEDEFAULT,
+                             r.right - r.left, r.bottom - r.top,
+                             NULL, NULL, inst, NULL);
+
+    {
+       HMENU bar = CreateMenu();
+       HMENU menu = CreateMenu();
+
+       AppendMenu(bar, MF_ENABLED|MF_POPUP, (UINT)menu, "Game");
+       AppendMenu(menu, MF_ENABLED, IDM_NEW, "New");
+       AppendMenu(menu, MF_ENABLED, IDM_RESTART, "Restart");
+
+       if ((fe->npresets = midend_num_presets(fe->me)) > 0) {
+           HMENU sub = CreateMenu();
+           int i;
+
+           AppendMenu(menu, MF_ENABLED|MF_POPUP, (UINT)sub, "Type");
+
+           fe->presets = snewn(fe->npresets, game_params *);
+
+           for (i = 0; i < fe->npresets; i++) {
+               char *name;
+
+               midend_fetch_preset(fe->me, i, &name, &fe->presets[i]);
+
+               /*
+                * FIXME: we ought to go through and do something
+                * with ampersands here.
+                */
+
+               AppendMenu(sub, MF_ENABLED, IDM_PRESETS + 0x10 * i, name);
+           }
+       }
+
+       AppendMenu(menu, MF_SEPARATOR, 0, 0);
+       AppendMenu(menu, MF_ENABLED, IDM_UNDO, "Undo");
+       AppendMenu(menu, MF_ENABLED, IDM_REDO, "Redo");
+       AppendMenu(menu, MF_SEPARATOR, 0, 0);
+       AppendMenu(menu, MF_ENABLED, IDM_QUIT, "Exit");
+       SetMenu(fe->hwnd, bar);
+    }
+
+    hdc = GetDC(fe->hwnd);
+    fe->bitmap = CreateCompatibleBitmap(hdc, x, y);
+    ReleaseDC(fe->hwnd, hdc);
+
+    SetWindowLong(fe->hwnd, GWL_USERDATA, (LONG)fe);
+
+    ShowWindow(fe->hwnd, SW_NORMAL);
+    SetForegroundWindow(fe->hwnd);
+
+    midend_redraw(fe->me);
+
+    return fe;
+}
+
+static LRESULT CALLBACK WndProc(HWND hwnd, UINT message,
+                               WPARAM wParam, LPARAM lParam)
+{
+    frontend *fe = (frontend *)GetWindowLong(hwnd, GWL_USERDATA);
+
+    switch (message) {
+      case WM_CLOSE:
+       DestroyWindow(hwnd);
+       return 0;
+      case WM_COMMAND:
+       switch (wParam & ~0xF) {       /* low 4 bits reserved to Windows */
+         case IDM_NEW:
+           if (!midend_process_key(fe->me, 0, 0, 'n'))
+               PostQuitMessage(0);
+           break;
+         case IDM_RESTART:
+           if (!midend_process_key(fe->me, 0, 0, 'r'))
+               PostQuitMessage(0);
+           break;
+         case IDM_UNDO:
+           if (!midend_process_key(fe->me, 0, 0, 'u'))
+               PostQuitMessage(0);
+           break;
+         case IDM_REDO:
+           if (!midend_process_key(fe->me, 0, 0, '\x12'))
+               PostQuitMessage(0);
+           break;
+         case IDM_QUIT:
+           if (!midend_process_key(fe->me, 0, 0, 'q'))
+               PostQuitMessage(0);
+           break;
+         default:
+           {
+               int p = ((wParam &~ 0xF) - IDM_PRESETS) / 0x10;
+
+               if (p >= 0 && p < fe->npresets) {
+                   RECT r;
+                   HDC hdc;
+                   int x, y;
+
+                   midend_set_params(fe->me, fe->presets[p]);
+                   midend_new_game(fe->me, NULL);
+                   midend_size(fe->me, &x, &y);
+
+                   r.left = r.top = 0;
+                   r.right = x;
+                   r.bottom = y;
+                   AdjustWindowRectEx(&r, WS_OVERLAPPEDWINDOW &~
+                                      (WS_THICKFRAME | WS_MAXIMIZEBOX |
+                                       WS_OVERLAPPED),
+                                      TRUE, 0);
+
+                   SetWindowPos(fe->hwnd, NULL, 0, 0,
+                                r.right - r.left, r.bottom - r.top,
+                                SWP_NOMOVE | SWP_NOZORDER);
+
+                   DeleteObject(fe->bitmap);
+
+                   hdc = GetDC(fe->hwnd);
+                   fe->bitmap = CreateCompatibleBitmap(hdc, x, y);
+                   ReleaseDC(fe->hwnd, hdc);
+
+                   midend_redraw(fe->me);
+               }
+           }
+           break;
+       }
+       break;
+      case WM_DESTROY:
+       PostQuitMessage(0);
+       return 0;
+      case WM_PAINT:
+       {
+           PAINTSTRUCT p;
+           HDC hdc, hdc2;
+           HBITMAP prevbm;
+
+           hdc = BeginPaint(hwnd, &p);
+           hdc2 = CreateCompatibleDC(hdc);
+           prevbm = SelectObject(hdc2, fe->bitmap);
+           BitBlt(hdc,
+                  p.rcPaint.left, p.rcPaint.top,
+                  p.rcPaint.right - p.rcPaint.left,
+                  p.rcPaint.bottom - p.rcPaint.top,
+                  hdc2,
+                  p.rcPaint.left, p.rcPaint.top,
+                  SRCCOPY);
+           SelectObject(hdc2, prevbm);
+           DeleteDC(hdc2);
+           EndPaint(hwnd, &p);
+       }
+       return 0;
+      case WM_KEYDOWN:
+       {
+           int key = -1;
+
+           switch (wParam) {
+             case VK_LEFT: key = CURSOR_LEFT; break;
+             case VK_RIGHT: key = CURSOR_RIGHT; break;
+             case VK_UP: key = CURSOR_UP; break;
+             case VK_DOWN: key = CURSOR_DOWN; break;
+               /*
+                * Diagonal keys on the numeric keypad.
+                */
+             case VK_PRIOR:
+               if (!(lParam & 0x01000000)) key = CURSOR_UP_RIGHT;
+               break;
+             case VK_NEXT:
+               if (!(lParam & 0x01000000)) key = CURSOR_DOWN_RIGHT;
+               break;
+             case VK_HOME:
+               if (!(lParam & 0x01000000)) key = CURSOR_UP_LEFT;
+               break;
+             case VK_END:
+               if (!(lParam & 0x01000000)) key = CURSOR_DOWN_LEFT;
+               break;
+               /*
+                * Numeric keypad keys with Num Lock on.
+                */
+             case VK_NUMPAD4: key = CURSOR_LEFT; break;
+             case VK_NUMPAD6: key = CURSOR_RIGHT; break;
+             case VK_NUMPAD8: key = CURSOR_UP; break;
+             case VK_NUMPAD2: key = CURSOR_DOWN; break;
+             case VK_NUMPAD9: key = CURSOR_UP_RIGHT; break;
+             case VK_NUMPAD3: key = CURSOR_DOWN_RIGHT; break;
+             case VK_NUMPAD7: key = CURSOR_UP_LEFT; break;
+             case VK_NUMPAD1: key = CURSOR_DOWN_LEFT; break;
+           }
+
+           if (key != -1) {
+               if (!midend_process_key(fe->me, 0, 0, key))
+                   PostQuitMessage(0);
+           } else {
+               MSG m;
+               m.hwnd = hwnd;
+               m.message = WM_KEYDOWN;
+               m.wParam = wParam;
+               m.lParam = lParam & 0xdfff;
+               TranslateMessage(&m);
+           }
+       }
+       break;
+      case WM_LBUTTONDOWN:
+      case WM_RBUTTONDOWN:
+      case WM_MBUTTONDOWN:
+       {
+           int button;
+
+           /*
+            * Shift-clicks count as middle-clicks, since otherwise
+            * two-button Windows users won't have any kind of
+            * middle click to use.
+            */
+           if (message == WM_MBUTTONDOWN || (wParam & MK_SHIFT))
+               button = MIDDLE_BUTTON;
+           else if (message == WM_LBUTTONDOWN)
+               button = LEFT_BUTTON;
+           else
+               button = RIGHT_BUTTON;
+               
+           if (!midend_process_key(fe->me, LOWORD(lParam),
+                                   HIWORD(lParam), button))
+               PostQuitMessage(0);
+       }
+       break;
+      case WM_CHAR:
+       if (!midend_process_key(fe->me, 0, 0, (unsigned char)wParam))
+           PostQuitMessage(0);
+       return 0;
+      case WM_TIMER:
+       if (fe->timer)
+           midend_timer(fe->me, (float)0.02);
+       return 0;
+    }
+
+    return DefWindowProc(hwnd, message, wParam, lParam);
+}
+
+int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmdline, int show)
+{
+    MSG msg;
+
+    srand(time(NULL));
+
+    if (!prev) {
+       WNDCLASS wndclass;
+
+       wndclass.style = 0;
+       wndclass.lpfnWndProc = WndProc;
+       wndclass.cbClsExtra = 0;
+       wndclass.cbWndExtra = 0;
+       wndclass.hInstance = inst;
+       wndclass.hIcon = LoadIcon(inst, IDI_APPLICATION);
+       wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
+       wndclass.hbrBackground = NULL;
+       wndclass.lpszMenuName = NULL;
+       wndclass.lpszClassName = game_name;
+
+       RegisterClass(&wndclass);
+    }
+
+    new_window(inst);
+
+    while (GetMessage(&msg, NULL, 0, 0)) {
+       DispatchMessage(&msg);
+    }
+
+    return msg.wParam;
+}