chiark / gitweb /
Implemented text and clipping primitives in the frontend, and added
[sgt-puzzles.git] / gtk.c
1 /*
2  * gtk.c: GTK front end for my puzzle collection.
3  */
4
5 #include <stdio.h>
6 #include <stdlib.h>
7 #include <time.h>
8 #include <stdarg.h>
9
10 #include <gtk/gtk.h>
11 #include <gdk/gdkkeysyms.h>
12
13 #include "puzzles.h"
14
15 /* ----------------------------------------------------------------------
16  * Error reporting functions used elsewhere.
17  */
18
19 void fatal(char *fmt, ...)
20 {
21     va_list ap;
22
23     fprintf(stderr, "fatal error: ");
24
25     va_start(ap, fmt);
26     vfprintf(stderr, fmt, ap);
27     va_end(ap);
28
29     fprintf(stderr, "\n");
30     exit(1);
31 }
32
33 /* ----------------------------------------------------------------------
34  * GTK front end to puzzles.
35  */
36
37 struct font {
38     GdkFont *font;
39     int type;
40     int size;
41 };
42
43 /*
44  * This structure holds all the data relevant to a single window.
45  * In principle this would allow us to open multiple independent
46  * puzzle windows, although I can't currently see any real point in
47  * doing so. I'm just coding cleanly because there's no
48  * particularly good reason not to.
49  */
50 struct frontend {
51     GtkWidget *window;
52     GtkWidget *area;
53     GdkPixmap *pixmap;
54     GdkColor *colours;
55     int ncolours;
56     GdkColormap *colmap;
57     int w, h;
58     midend_data *me;
59     GdkGC *gc;
60     int bbox_l, bbox_r, bbox_u, bbox_d;
61     int timer_active, timer_id;
62     struct font *fonts;
63     int nfonts, fontsize;
64 };
65
66 void frontend_default_colour(frontend *fe, float *output)
67 {
68     GdkColor col = fe->window->style->bg[GTK_STATE_NORMAL];
69     output[0] = col.red / 65535.0;
70     output[1] = col.green / 65535.0;
71     output[2] = col.blue / 65535.0;
72 }
73
74 void start_draw(frontend *fe)
75 {
76     fe->gc = gdk_gc_new(fe->area->window);
77     fe->bbox_l = fe->w;
78     fe->bbox_r = 0;
79     fe->bbox_u = fe->h;
80     fe->bbox_d = 0;
81 }
82
83 void clip(frontend *fe, int x, int y, int w, int h)
84 {
85     GdkRectangle rect;
86
87     rect.x = x;
88     rect.y = y;
89     rect.width = w;
90     rect.height = h;
91
92     gdk_gc_set_clip_rectangle(fe->gc, &rect);
93 }
94
95 void unclip(frontend *fe)
96 {
97     GdkRectangle rect;
98
99     rect.x = 0;
100     rect.y = 0;
101     rect.width = fe->w;
102     rect.height = fe->h;
103
104     gdk_gc_set_clip_rectangle(fe->gc, &rect);
105 }
106
107 void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize,
108                int align, int colour, char *text)
109 {
110     int i;
111
112     /*
113      * Find or create the font.
114      */
115     for (i = 0; i < fe->nfonts; i++)
116         if (fe->fonts[i].type == fonttype && fe->fonts[i].size == fontsize)
117             break;
118
119     if (i == fe->nfonts) {
120         if (fe->fontsize <= fe->nfonts) {
121             fe->fontsize = fe->nfonts + 10;
122             fe->fonts = sresize(fe->fonts, fe->fontsize, struct font);
123         }
124
125         fe->nfonts++;
126
127         fe->fonts[i].type = fonttype;
128         fe->fonts[i].size = fontsize;
129
130         /*
131          * FIXME: Really I should make at least _some_ effort to
132          * pick the correct font.
133          */
134         fe->fonts[i].font = gdk_font_load("variable");
135     }
136
137     /*
138      * Find string dimensions and process alignment.
139      */
140     {
141         int lb, rb, wid, asc, desc;
142
143         gdk_string_extents(fe->fonts[i].font, text,
144                            &lb, &rb, &wid, &asc, &desc);
145         if (align & ALIGN_VCENTRE)
146             y += asc - (asc+desc)/2;
147
148         if (align & ALIGN_HCENTRE)
149             x -= wid / 2;
150         else if (align & ALIGN_HRIGHT)
151             x -= wid;
152
153     }
154
155     /*
156      * Set colour and actually draw text.
157      */
158     gdk_gc_set_foreground(fe->gc, &fe->colours[colour]);
159     gdk_draw_string(fe->pixmap, fe->fonts[i].font, fe->gc, x, y, text);
160 }
161
162 void draw_rect(frontend *fe, int x, int y, int w, int h, int colour)
163 {
164     gdk_gc_set_foreground(fe->gc, &fe->colours[colour]);
165     gdk_draw_rectangle(fe->pixmap, fe->gc, 1, x, y, w, h);
166 }
167
168 void draw_line(frontend *fe, int x1, int y1, int x2, int y2, int colour)
169 {
170     gdk_gc_set_foreground(fe->gc, &fe->colours[colour]);
171     gdk_draw_line(fe->pixmap, fe->gc, x1, y1, x2, y2);
172 }
173
174 void draw_polygon(frontend *fe, int *coords, int npoints,
175                   int fill, int colour)
176 {
177     GdkPoint *points = snewn(npoints, GdkPoint);
178     int i;
179
180     for (i = 0; i < npoints; i++) {
181         points[i].x = coords[i*2];
182         points[i].y = coords[i*2+1];
183     }
184
185     gdk_gc_set_foreground(fe->gc, &fe->colours[colour]);
186     gdk_draw_polygon(fe->pixmap, fe->gc, fill, points, npoints);
187
188     sfree(points);
189 }
190
191 void draw_update(frontend *fe, int x, int y, int w, int h)
192 {
193     if (fe->bbox_l > x  ) fe->bbox_l = x  ;
194     if (fe->bbox_r < x+w) fe->bbox_r = x+w;
195     if (fe->bbox_u > y  ) fe->bbox_u = y  ;
196     if (fe->bbox_d < y+h) fe->bbox_d = y+h;
197 }
198
199 void end_draw(frontend *fe)
200 {
201     gdk_gc_unref(fe->gc);
202     fe->gc = NULL;
203
204     if (fe->bbox_l < fe->bbox_r && fe->bbox_u < fe->bbox_d) {
205         gdk_draw_pixmap(fe->area->window,
206                         fe->area->style->fg_gc[GTK_WIDGET_STATE(fe->area)],
207                         fe->pixmap,
208                         fe->bbox_l, fe->bbox_u,
209                         fe->bbox_l, fe->bbox_u,
210                         fe->bbox_r - fe->bbox_l, fe->bbox_d - fe->bbox_u);
211     }
212 }
213
214 static void destroy(GtkWidget *widget, gpointer data)
215 {
216     gtk_main_quit();
217 }
218
219 static gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data)
220 {
221     frontend *fe = (frontend *)data;
222     int keyval;
223
224     if (!fe->pixmap)
225         return TRUE;
226
227     if (event->string[0] && !event->string[1])
228         keyval = (unsigned char)event->string[0];
229     else if (event->keyval == GDK_Up || event->keyval == GDK_KP_Up ||
230              event->keyval == GDK_KP_8)
231         keyval = CURSOR_UP;
232     else if (event->keyval == GDK_Down || event->keyval == GDK_KP_Down ||
233              event->keyval == GDK_KP_2)
234         keyval = CURSOR_DOWN;
235     else if (event->keyval == GDK_Left || event->keyval == GDK_KP_Left ||
236              event->keyval == GDK_KP_4)
237         keyval = CURSOR_LEFT;
238     else if (event->keyval == GDK_Right || event->keyval == GDK_KP_Right ||
239              event->keyval == GDK_KP_6)
240         keyval = CURSOR_RIGHT;
241     else if (event->keyval == GDK_KP_Home || event->keyval == GDK_KP_7)
242         keyval = CURSOR_UP_LEFT;
243     else if (event->keyval == GDK_KP_End || event->keyval == GDK_KP_1)
244         keyval = CURSOR_DOWN_LEFT;
245     else if (event->keyval == GDK_KP_Page_Up || event->keyval == GDK_KP_9)
246         keyval = CURSOR_UP_RIGHT;
247     else if (event->keyval == GDK_KP_Page_Down || event->keyval == GDK_KP_3)
248         keyval = CURSOR_DOWN_RIGHT;
249     else
250         keyval = -1;
251
252     if (keyval >= 0 &&
253         !midend_process_key(fe->me, 0, 0, keyval))
254         gtk_widget_destroy(fe->window);
255
256     return TRUE;
257 }
258
259 static gint button_event(GtkWidget *widget, GdkEventButton *event,
260                          gpointer data)
261 {
262     frontend *fe = (frontend *)data;
263     int button;
264
265     if (!fe->pixmap)
266         return TRUE;
267
268     if (event->type != GDK_BUTTON_PRESS)
269         return TRUE;
270
271     if (event->button == 2 || (event->state & GDK_SHIFT_MASK))
272         button = MIDDLE_BUTTON;
273     else if (event->button == 1)
274         button = LEFT_BUTTON;
275     else if (event->button == 3)
276         button = RIGHT_BUTTON;
277     else
278         return FALSE;                  /* don't even know what button! */
279
280     if (!midend_process_key(fe->me, event->x, event->y, button))
281         gtk_widget_destroy(fe->window);
282
283     return TRUE;
284 }
285
286 static gint expose_area(GtkWidget *widget, GdkEventExpose *event,
287                         gpointer data)
288 {
289     frontend *fe = (frontend *)data;
290
291     if (fe->pixmap) {
292         gdk_draw_pixmap(widget->window,
293                         widget->style->fg_gc[GTK_WIDGET_STATE(widget)],
294                         fe->pixmap,
295                         event->area.x, event->area.y,
296                         event->area.x, event->area.y,
297                         event->area.width, event->area.height);
298     }
299     return TRUE;
300 }
301
302 static gint configure_area(GtkWidget *widget,
303                            GdkEventConfigure *event, gpointer data)
304 {
305     frontend *fe = (frontend *)data;
306     GdkGC *gc;
307
308     if (fe->pixmap)
309         gdk_pixmap_unref(fe->pixmap);
310
311     fe->pixmap = gdk_pixmap_new(widget->window, fe->w, fe->h, -1);
312
313     gc = gdk_gc_new(fe->area->window);
314     gdk_gc_set_foreground(gc, &fe->colours[0]);
315     gdk_draw_rectangle(fe->pixmap, gc, 1, 0, 0, fe->w, fe->h);
316     gdk_gc_unref(gc);
317
318     midend_redraw(fe->me);
319
320     return TRUE;
321 }
322
323 static gint timer_func(gpointer data)
324 {
325     frontend *fe = (frontend *)data;
326
327     if (fe->timer_active)
328         midend_timer(fe->me, 0.02);    /* may clear timer_active */
329
330     return fe->timer_active;
331 }
332
333 void deactivate_timer(frontend *fe)
334 {
335     if (fe->timer_active)
336         gtk_timeout_remove(fe->timer_id);
337     fe->timer_active = FALSE;
338 }
339
340 void activate_timer(frontend *fe)
341 {
342     if (!fe->timer_active)
343         fe->timer_id = gtk_timeout_add(20, timer_func, fe);
344     fe->timer_active = TRUE;
345 }
346
347 static void menu_key_event(GtkMenuItem *menuitem, gpointer data)
348 {
349     frontend *fe = (frontend *)data;
350     int key = GPOINTER_TO_INT(gtk_object_get_data(GTK_OBJECT(menuitem),
351                                                   "user-data"));
352     if (!midend_process_key(fe->me, 0, 0, key))
353         gtk_widget_destroy(fe->window);
354 }
355
356 static void menu_preset_event(GtkMenuItem *menuitem, gpointer data)
357 {
358     frontend *fe = (frontend *)data;
359     game_params *params =
360         (game_params *)gtk_object_get_data(GTK_OBJECT(menuitem), "user-data");
361     int x, y;
362
363     midend_set_params(fe->me, params);
364     midend_new_game(fe->me, NULL);
365     midend_size(fe->me, &x, &y);
366     gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
367     fe->w = x;
368     fe->h = y;
369 }
370
371 static GtkWidget *add_menu_item_with_key(frontend *fe, GtkContainer *cont,
372                                          char *text, int key)
373 {
374     GtkWidget *menuitem = gtk_menu_item_new_with_label(text);
375     gtk_container_add(cont, menuitem);
376     gtk_object_set_data(GTK_OBJECT(menuitem), "user-data",
377                         GINT_TO_POINTER(key));
378     gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
379                        GTK_SIGNAL_FUNC(menu_key_event), fe);
380     gtk_widget_show(menuitem);
381     return menuitem;
382 }
383
384 static void add_menu_separator(GtkContainer *cont)
385 {
386     GtkWidget *menuitem = gtk_menu_item_new();
387     gtk_container_add(cont, menuitem);
388     gtk_widget_show(menuitem);
389 }
390
391 static frontend *new_window(void)
392 {
393     frontend *fe;
394     GtkBox *vbox;
395     GtkWidget *menubar, *menu, *menuitem;
396     int x, y, n;
397
398     fe = snew(frontend);
399
400     fe->me = midend_new(fe);
401     midend_new_game(fe->me, NULL);
402
403     fe->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
404     gtk_window_set_title(GTK_WINDOW(fe->window), game_name);
405 #if 0
406     gtk_window_set_resizable(GTK_WINDOW(fe->window), FALSE);
407 #else
408     gtk_window_set_policy(GTK_WINDOW(fe->window), FALSE, FALSE, TRUE);
409 #endif
410     vbox = GTK_BOX(gtk_vbox_new(FALSE, 0));
411     gtk_container_add(GTK_CONTAINER(fe->window), GTK_WIDGET(vbox));
412     gtk_widget_show(GTK_WIDGET(vbox));
413
414     menubar = gtk_menu_bar_new();
415     gtk_box_pack_start(vbox, menubar, FALSE, FALSE, 0);
416     gtk_widget_show(menubar);
417
418     menuitem = gtk_menu_item_new_with_label("Game");
419     gtk_container_add(GTK_CONTAINER(menubar), menuitem);
420     gtk_widget_show(menuitem);
421
422     menu = gtk_menu_new();
423     gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), menu);
424
425     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "New", 'n');
426     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Restart", 'r');
427
428     if ((n = midend_num_presets(fe->me)) > 0) {
429         GtkWidget *submenu;
430         int i;
431
432         menuitem = gtk_menu_item_new_with_label("Type");
433         gtk_container_add(GTK_CONTAINER(menu), menuitem);
434         gtk_widget_show(menuitem);
435
436         submenu = gtk_menu_new();
437         gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), submenu);
438
439         for (i = 0; i < n; i++) {
440             char *name;
441             game_params *params;
442
443             midend_fetch_preset(fe->me, i, &name, &params);
444
445             menuitem = gtk_menu_item_new_with_label(name);
446             gtk_container_add(GTK_CONTAINER(submenu), menuitem);
447             gtk_object_set_data(GTK_OBJECT(menuitem), "user-data", params);
448             gtk_signal_connect(GTK_OBJECT(menuitem), "activate",
449                                GTK_SIGNAL_FUNC(menu_preset_event), fe);
450             gtk_widget_show(menuitem);
451         }
452     }
453
454     add_menu_separator(GTK_CONTAINER(menu));
455     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Undo", 'u');
456     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Redo", '\x12');
457     add_menu_separator(GTK_CONTAINER(menu));
458     add_menu_item_with_key(fe, GTK_CONTAINER(menu), "Exit", 'q');
459
460     {
461         int i, ncolours;
462         float *colours;
463         gboolean *success;
464
465         fe->colmap = gdk_colormap_get_system();
466         colours = midend_colours(fe->me, &ncolours);
467         fe->ncolours = ncolours;
468         fe->colours = snewn(ncolours, GdkColor);
469         for (i = 0; i < ncolours; i++) {
470             fe->colours[i].red = colours[i*3] * 0xFFFF;
471             fe->colours[i].green = colours[i*3+1] * 0xFFFF;
472             fe->colours[i].blue = colours[i*3+2] * 0xFFFF;
473         }
474         success = snewn(ncolours, gboolean);
475         gdk_colormap_alloc_colors(fe->colmap, fe->colours, ncolours,
476                                   FALSE, FALSE, success);
477         for (i = 0; i < ncolours; i++) {
478             if (!success[i])
479                 g_error("couldn't allocate colour %d (#%02x%02x%02x)\n",
480                         i, fe->colours[i].red >> 8,
481                         fe->colours[i].green >> 8,
482                         fe->colours[i].blue >> 8);
483         }
484     }
485
486     fe->area = gtk_drawing_area_new();
487     midend_size(fe->me, &x, &y);
488     gtk_drawing_area_size(GTK_DRAWING_AREA(fe->area), x, y);
489     fe->w = x;
490     fe->h = y;
491
492     gtk_box_pack_end(vbox, fe->area, FALSE, FALSE, 0);
493
494     fe->pixmap = NULL;
495     fe->fonts = NULL;
496     fe->nfonts = fe->fontsize = 0;
497
498     fe->timer_active = FALSE;
499
500     gtk_signal_connect(GTK_OBJECT(fe->window), "destroy",
501                        GTK_SIGNAL_FUNC(destroy), fe);
502     gtk_signal_connect(GTK_OBJECT(fe->window), "key_press_event",
503                        GTK_SIGNAL_FUNC(key_event), fe);
504     gtk_signal_connect(GTK_OBJECT(fe->area), "button_press_event",
505                        GTK_SIGNAL_FUNC(button_event), fe);
506     gtk_signal_connect(GTK_OBJECT(fe->area), "expose_event",
507                        GTK_SIGNAL_FUNC(expose_area), fe);
508     gtk_signal_connect(GTK_OBJECT(fe->area), "configure_event",
509                        GTK_SIGNAL_FUNC(configure_area), fe);
510
511     gtk_widget_add_events(GTK_WIDGET(fe->area), GDK_BUTTON_PRESS_MASK);
512
513     gtk_widget_show(fe->area);
514     gtk_widget_show(fe->window);
515
516     return fe;
517 }
518
519 int main(int argc, char **argv)
520 {
521     srand(time(NULL));
522
523     gtk_init(&argc, &argv);
524     (void) new_window();
525     gtk_main();
526
527     return 0;
528 }