share/gui: add label truncation
[neverball] / share / gui.c
1 /*
2  * Copyright (C) 2003 Robert Kooima
3  *
4  * NEVERBALL is  free software; you can redistribute  it and/or modify
5  * it under the  terms of the GNU General  Public License as published
6  * by the Free  Software Foundation; either version 2  of the License,
7  * or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful, but
10  * WITHOUT  ANY  WARRANTY;  without   even  the  implied  warranty  of
11  * MERCHANTABILITY or  FITNESS FOR A PARTICULAR PURPOSE.   See the GNU
12  * General Public License for more details.
13  */
14
15 #include <stdlib.h>
16 #include <string.h>
17 #include <stdio.h>
18
19 #include "config.h"
20 #include "video.h"
21 #include "glext.h"
22 #include "image.h"
23 #include "vec3.h"
24 #include "gui.h"
25 #include "common.h"
26
27 #include "fs.h"
28 #include "fs_rwops.h"
29
30 /*---------------------------------------------------------------------------*/
31
32 #define MAXWIDGET 256
33
34 #define GUI_TYPE 0xFFFE
35
36 #define GUI_FREE   0
37 #define GUI_STATE  1
38 #define GUI_HARRAY 2
39 #define GUI_VARRAY 4
40 #define GUI_HSTACK 6
41 #define GUI_VSTACK 8
42 #define GUI_FILLER 10
43 #define GUI_IMAGE  12
44 #define GUI_LABEL  14
45 #define GUI_COUNT  16
46 #define GUI_CLOCK  18
47 #define GUI_SPACE  20
48
49 struct widget
50 {
51     int     type;
52     int     token;
53     int     value;
54     int     size;
55     int     rect;
56
57     int     x, y;
58     int     w, h;
59     int     car;
60     int     cdr;
61
62     GLuint  text_img;
63     GLuint  text_obj;
64     GLuint  rect_obj;
65
66     const GLfloat *color0;
67     const GLfloat *color1;
68
69     GLfloat  scale;
70
71     int text_obj_w;
72     int text_obj_h;
73
74     enum trunc trunc;
75 };
76
77 /*---------------------------------------------------------------------------*/
78
79 const GLfloat gui_wht[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
80 const GLfloat gui_yel[4] = { 1.0f, 1.0f, 0.0f, 1.0f };
81 const GLfloat gui_red[4] = { 1.0f, 0.0f, 0.0f, 1.0f };
82 const GLfloat gui_grn[4] = { 0.0f, 1.0f, 0.0f, 1.0f };
83 const GLfloat gui_blu[4] = { 0.0f, 0.0f, 1.0f, 1.0f };
84 const GLfloat gui_blk[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
85 const GLfloat gui_gry[4] = { 0.3f, 0.3f, 0.3f, 1.0f };
86
87 /*---------------------------------------------------------------------------*/
88
89 static struct widget widget[MAXWIDGET];
90 static int           active;
91 static int           radius;
92 static TTF_Font     *font[3] = { NULL, NULL, NULL };
93
94 static void      *fontdata;
95 static int        fontdatalen;
96 static SDL_RWops *fontrwops;
97
98 static GLuint digit_text[3][11];
99 static GLuint digit_list[3][11];
100 static int    digit_w[3][11];
101 static int    digit_h[3][11];
102
103 /*---------------------------------------------------------------------------*/
104
105 static int gui_hot(int id)
106 {
107     return (widget[id].type & GUI_STATE);
108 }
109
110 /*---------------------------------------------------------------------------*/
111 /*
112  * Initialize a  display list  containing a  rectangle (x, y, w, h) to
113  * which a  rendered-font texture  may be applied.   Colors  c0 and c1
114  * determine the top-to-bottom color gradient of the text.
115  */
116
117 static GLuint gui_list(int x, int y,
118                        int w, int h, const float *c0, const float *c1)
119 {
120     GLuint list = glGenLists(1);
121
122     GLfloat s0, t0;
123     GLfloat s1, t1;
124
125     int W, H, ww, hh, d = h / 16;
126
127     /* Assume the applied texture size is rect size rounded to power-of-two. */
128
129     image_size(&W, &H, w, h);
130
131     ww = ((W - w) % 2) ? w + 1 : w;
132     hh = ((H - h) % 2) ? h + 1 : h;
133
134     s0 = 0.5f * (W - ww) / W;
135     t0 = 0.5f * (H - hh) / H;
136     s1 = 1.0f - s0;
137     t1 = 1.0f - t0;
138
139     glNewList(list, GL_COMPILE);
140     {
141         glBegin(GL_QUADS);
142         {
143             glColor4f(0.0f, 0.0f, 0.0f, 0.5f);
144             glTexCoord2f(s0, t1); glVertex2i(x      + d, y      - d);
145             glTexCoord2f(s1, t1); glVertex2i(x + ww + d, y      - d);
146             glTexCoord2f(s1, t0); glVertex2i(x + ww + d, y + hh - d);
147             glTexCoord2f(s0, t0); glVertex2i(x      + d, y + hh - d);
148
149             glColor4fv(c0);
150             glTexCoord2f(s0, t1); glVertex2i(x,      y);
151             glTexCoord2f(s1, t1); glVertex2i(x + ww, y);
152
153             glColor4fv(c1);
154             glTexCoord2f(s1, t0); glVertex2i(x + ww, y + hh);
155             glTexCoord2f(s0, t0); glVertex2i(x,      y + hh);
156         }
157         glEnd();
158     }
159     glEndList();
160
161     return list;
162 }
163
164 /*
165  * Initialize a display list containing a rounded-corner rectangle (x,
166  * y, w, h).  Generate texture coordinates to properly apply a texture
167  * map to the rectangle as though the corners were not rounded.
168  */
169
170 static GLuint gui_rect(int x, int y, int w, int h, int f, int r)
171 {
172     GLuint list = glGenLists(1);
173
174     int n = 8;
175     int i;
176
177     glNewList(list, GL_COMPILE);
178     {
179         glBegin(GL_QUAD_STRIP);
180         {
181             /* Left side... */
182
183             for (i = 0; i <= n; i++)
184             {
185                 float a = 0.5f * V_PI * (float) i / (float) n;
186                 float s = r * fsinf(a);
187                 float c = r * fcosf(a);
188
189                 float X  = x     + r - c;
190                 float Ya = y + h + ((f & GUI_NW) ? (s - r) : 0);
191                 float Yb = y     + ((f & GUI_SW) ? (r - s) : 0);
192
193                 glTexCoord2f((X - x) / w, (Ya - y) / h);
194                 glVertex2f(X, Ya);
195
196                 glTexCoord2f((X - x) / w, (Yb - y) / h);
197                 glVertex2f(X, Yb);
198             }
199
200             /* ... Right side. */
201
202             for (i = 0; i <= n; i++)
203             {
204                 float a = 0.5f * V_PI * (float) i / (float) n;
205                 float s = r * fsinf(a);
206                 float c = r * fcosf(a);
207
208                 float X  = x + w - r + s;
209                 float Ya = y + h + ((f & GUI_NE) ? (c - r) : 0);
210                 float Yb = y     + ((f & GUI_SE) ? (r - c) : 0);
211
212                 glTexCoord2f((X - x) / w, (Ya - y) / h);
213                 glVertex2f(X, Ya);
214
215                 glTexCoord2f((X - x) / w, (Yb - y) / h);
216                 glVertex2f(X, Yb);
217             }
218         }
219         glEnd();
220     }
221     glEndList();
222
223     return list;
224 }
225
226 /*---------------------------------------------------------------------------*/
227
228 static const char *pick_font_path(void)
229 {
230     const char *path;
231
232     path = _(GUI_FACE);
233
234     if (!fs_exists(path))
235     {
236         fprintf(stderr, _("Font \"%s\" doesn't exist, trying default font.\n"),
237                 path);
238
239         path = GUI_FACE;
240     }
241
242     return path;
243 }
244
245 void gui_init(void)
246 {
247     const float *c0 = gui_yel;
248     const float *c1 = gui_red;
249
250     int w = config_get_d(CONFIG_WIDTH);
251     int h = config_get_d(CONFIG_HEIGHT);
252     int i, j, s = (h < w) ? h : w;
253
254     /* Initialize font rendering. */
255
256     if (TTF_Init() == 0)
257     {
258         const char *fontpath = pick_font_path();
259
260         int s0 = s / 26;
261         int s1 = s / 13;
262         int s2 = s /  7;
263
264         memset(widget, 0, sizeof (struct widget) * MAXWIDGET);
265
266         /* Load the font. */
267
268         if (!(fontdata = fs_load(fontpath, &fontdatalen)))
269         {
270             fprintf(stderr, _("Could not load font %s.\n"), fontpath);
271             /* Return or no return, we'll probably crash now. */
272             return;
273         }
274
275         fontrwops = SDL_RWFromConstMem(fontdata, fontdatalen);
276
277         /* Load small, medium, and large typefaces. */
278
279         font[GUI_SML] = TTF_OpenFontRW(fontrwops, 0, s0);
280
281         SDL_RWseek(fontrwops, 0, SEEK_SET);
282         font[GUI_MED] = TTF_OpenFontRW(fontrwops, 0, s1);
283
284         SDL_RWseek(fontrwops, 0, SEEK_SET);
285         font[GUI_LRG] = TTF_OpenFontRW(fontrwops, 0, s2);
286
287         /* fontrwops remains open. */
288
289         radius = s / 60;
290
291         /* Initialize digit glyphs and lists for counters and clocks. */
292
293         for (i = 0; i < 3; i++)
294         {
295             char text[2];
296
297             /* Draw digits 0 through 9. */
298
299             for (j = 0; j < 10; j++)
300             {
301                 text[0] = '0' + (char) j;
302                 text[1] =  0;
303
304                 digit_text[i][j] = make_image_from_font(NULL, NULL,
305                                                         &digit_w[i][j],
306                                                         &digit_h[i][j],
307                                                         text, font[i]);
308                 digit_list[i][j] = gui_list(-digit_w[i][j] / 2,
309                                             -digit_h[i][j] / 2,
310                                             +digit_w[i][j],
311                                             +digit_h[i][j], c0, c1);
312             }
313
314             /* Draw the colon for the clock. */
315
316             digit_text[i][j] = make_image_from_font(NULL, NULL,
317                                                     &digit_w[i][10],
318                                                     &digit_h[i][10],
319                                                     ":", font[i]);
320             digit_list[i][j] = gui_list(-digit_w[i][10] / 2,
321                                         -digit_h[i][10] / 2,
322                                         +digit_w[i][10],
323                                         +digit_h[i][10], c0, c1);
324         }
325     }
326
327     active = 0;
328 }
329
330 void gui_free(void)
331 {
332     int i, j, id;
333
334     /* Release any remaining widget texture and display list indices. */
335
336     for (id = 1; id < MAXWIDGET; id++)
337     {
338         if (glIsTexture(widget[id].text_img))
339             glDeleteTextures(1, &widget[id].text_img);
340
341         if (glIsList(widget[id].text_obj))
342             glDeleteLists(widget[id].text_obj, 1);
343         if (glIsList(widget[id].rect_obj))
344             glDeleteLists(widget[id].rect_obj, 1);
345
346         widget[id].type     = GUI_FREE;
347         widget[id].text_img = 0;
348         widget[id].text_obj = 0;
349         widget[id].rect_obj = 0;
350         widget[id].cdr      = 0;
351         widget[id].car      = 0;
352     }
353
354     /* Release all digit textures and display lists. */
355
356     for (i = 0; i < 3; i++)
357         for (j = 0; j < 11; j++)
358         {
359             if (glIsTexture(digit_text[i][j]))
360                 glDeleteTextures(1, &digit_text[i][j]);
361
362             if (glIsList(digit_list[i][j]))
363                 glDeleteLists(digit_list[i][j], 1);
364         }
365
366     /* Release all loaded fonts and finalize font rendering. */
367
368     if (font[GUI_LRG]) TTF_CloseFont(font[GUI_LRG]);
369     if (font[GUI_MED]) TTF_CloseFont(font[GUI_MED]);
370     if (font[GUI_SML]) TTF_CloseFont(font[GUI_SML]);
371
372     if (fontrwops) SDL_RWclose(fontrwops);
373     if (fontdata)  free(fontdata);
374
375     TTF_Quit();
376 }
377
378 /*---------------------------------------------------------------------------*/
379
380 static int gui_widget(int pd, int type)
381 {
382     int id;
383
384     /* Find an unused entry in the widget table. */
385
386     for (id = 1; id < MAXWIDGET; id++)
387         if (widget[id].type == GUI_FREE)
388         {
389             /* Set the type and default properties. */
390
391             widget[id].type     = type;
392             widget[id].token    = 0;
393             widget[id].value    = 0;
394             widget[id].size     = 0;
395             widget[id].rect     = GUI_NW | GUI_SW | GUI_NE | GUI_SE;
396             widget[id].w        = 0;
397             widget[id].h        = 0;
398             widget[id].text_img = 0;
399             widget[id].text_obj = 0;
400             widget[id].rect_obj = 0;
401             widget[id].color0   = gui_wht;
402             widget[id].color1   = gui_wht;
403             widget[id].scale    = 1.0f;
404             widget[id].trunc    = TRUNC_NONE;
405
406             widget[id].text_obj_w = 0;
407             widget[id].text_obj_h = 0;
408
409             /* Insert the new widget into the parent's widget list. */
410
411             if (pd)
412             {
413                 widget[id].car = 0;
414                 widget[id].cdr = widget[pd].car;
415                 widget[pd].car = id;
416             }
417             else
418             {
419                 widget[id].car = 0;
420                 widget[id].cdr = 0;
421             }
422
423             return id;
424         }
425
426     fprintf(stderr, "Out of widget IDs\n");
427
428     return 0;
429 }
430
431 int gui_harray(int pd) { return gui_widget(pd, GUI_HARRAY); }
432 int gui_varray(int pd) { return gui_widget(pd, GUI_VARRAY); }
433 int gui_hstack(int pd) { return gui_widget(pd, GUI_HSTACK); }
434 int gui_vstack(int pd) { return gui_widget(pd, GUI_VSTACK); }
435 int gui_filler(int pd) { return gui_widget(pd, GUI_FILLER); }
436
437 /*---------------------------------------------------------------------------*/
438
439 struct size
440 {
441     int w, h;
442 };
443
444
445 static struct size gui_measure(const char *text, TTF_Font *font)
446 {
447     struct size size = { 0, 0 };
448     TTF_SizeUTF8(font, text, &size.w, &size.h);
449     return size;
450 }
451
452 static char *gui_trunc_head(const char *text,
453                             const int maxwidth,
454                             TTF_Font *font)
455 {
456     int left, right, mid;
457     char *str = NULL;
458
459     left  = 0;
460     right = strlen(text);
461
462     while (right - left > 1)
463     {
464         mid = (left + right) / 2;
465
466         str = concat_string("...", text + mid, NULL);
467
468         if (gui_measure(str, font).w <= maxwidth)
469             right = mid;
470         else
471             left = mid;
472
473         free(str);
474     }
475
476     return concat_string("...", text + left, NULL);
477 }
478
479 static char *gui_trunc_tail(const char *text,
480                             const int maxwidth,
481                             TTF_Font *font)
482 {
483     int left, right, mid;
484     char *str = NULL;
485
486     left  = 0;
487     right = strlen(text);
488
489     while (right - left > 1)
490     {
491         mid = (left + right) / 2;
492
493         str = malloc(mid + sizeof ("..."));
494
495         memcpy(str,       text,  mid);
496         memcpy(str + mid, "...", sizeof ("..."));
497
498         if (gui_measure(str, font).w <= maxwidth)
499             left = mid;
500         else
501             right = mid;
502
503         free(str);
504     }
505
506     str = malloc(right + sizeof ("..."));
507
508     memcpy(str,         text,  right);
509     memcpy(str + right, "...", sizeof ("..."));
510
511     return str;
512 }
513
514 static char *gui_truncate(const char *text,
515                           const int maxwidth,
516                           TTF_Font *font,
517                           enum trunc trunc)
518 {
519     if (gui_measure(text, font).w <= maxwidth)
520         return strdup(text);
521
522     switch (trunc)
523     {
524     case TRUNC_NONE: return strdup(text);                         break;
525     case TRUNC_HEAD: return gui_trunc_head(text, maxwidth, font); break;
526     case TRUNC_TAIL: return gui_trunc_tail(text, maxwidth, font); break;
527     }
528
529     return NULL;
530 }
531
532 /*---------------------------------------------------------------------------*/
533
534 void gui_set_image(int id, const char *file)
535 {
536     if (glIsTexture(widget[id].text_img))
537         glDeleteTextures(1, &widget[id].text_img);
538
539     widget[id].text_img = make_image_from_file(file);
540 }
541
542 void gui_set_label(int id, const char *text)
543 {
544     int w, h;
545
546     if (glIsTexture(widget[id].text_img))
547         glDeleteTextures(1, &widget[id].text_img);
548     if (glIsList(widget[id].text_obj))
549         glDeleteLists(widget[id].text_obj, 1);
550
551     text = gui_truncate(text, widget[id].w - radius * 2,
552                         font[widget[id].size],
553                         widget[id].trunc);
554
555     widget[id].text_img = make_image_from_font(NULL, NULL, &w, &h,
556                                                text, font[widget[id].size]);
557     widget[id].text_obj = gui_list(-w / 2, -h / 2, w, h,
558                                    widget[id].color0, widget[id].color1);
559
560     widget[id].text_obj_w = w;
561     widget[id].text_obj_h = h;
562
563     free((void *) text);
564 }
565
566 void gui_set_count(int id, int value)
567 {
568     widget[id].value = value;
569 }
570
571 void gui_set_clock(int id, int value)
572 {
573     widget[id].value = value;
574 }
575
576 void gui_set_color(int id, const float *c0,
577                            const float *c1)
578 {
579     if (id)
580     {
581         c0 = c0 ? c0 : gui_yel;
582         c1 = c1 ? c1 : gui_red;
583
584         if (widget[id].color0 != c0 || widget[id].color1 != c1)
585         {
586             widget[id].color0 = c0;
587             widget[id].color1 = c1;
588
589             if (glIsList(widget[id].text_obj))
590             {
591                 int w, h;
592
593                 glDeleteLists(widget[id].text_obj, 1);
594
595                 w = widget[id].text_obj_w;
596                 h = widget[id].text_obj_h;
597
598                 widget[id].text_obj = gui_list(-w / 2, -h / 2, w, h,
599                                                widget[id].color0,
600                                                widget[id].color1);
601             }
602         }
603     }
604 }
605
606 void gui_set_multi(int id, const char *text)
607 {
608     const char *p;
609
610     char s[8][MAXSTR];
611     int  i, j, jd;
612
613     size_t n = 0;
614
615     /* Copy each delimited string to a line buffer. */
616
617     for (p = text, j = 0; *p && j < 8; j++)
618     {
619         strncpy(s[j], p, (n = strcspn(p, "\\")));
620         s[j][n] = 0;
621
622         if (*(p += n) == '\\') p++;
623     }
624
625     /* Set the label value for each line. */
626
627     for (i = j - 1, jd = widget[id].car; i >= 0 && jd; i--, jd = widget[jd].cdr)
628         gui_set_label(jd, s[i]);
629 }
630
631 void gui_set_trunc(int id, enum trunc trunc)
632 {
633     widget[id].trunc = trunc;
634 }
635
636 /*---------------------------------------------------------------------------*/
637
638 int gui_image(int pd, const char *file, int w, int h)
639 {
640     int id;
641
642     if ((id = gui_widget(pd, GUI_IMAGE)))
643     {
644         widget[id].text_img = make_image_from_file(file);
645         widget[id].w = w;
646         widget[id].h = h;
647     }
648     return id;
649 }
650
651 int gui_start(int pd, const char *text, int size, int token, int value)
652 {
653     int id;
654
655     if ((id = gui_state(pd, text, size, token, value)))
656         active = id;
657
658     return id;
659 }
660
661 int gui_state(int pd, const char *text, int size, int token, int value)
662 {
663     int id;
664
665     if ((id = gui_widget(pd, GUI_STATE)))
666     {
667         widget[id].text_img = make_image_from_font(NULL, NULL,
668                                                    &widget[id].w,
669                                                    &widget[id].h,
670                                                    text, font[size]);
671         widget[id].size  = size;
672         widget[id].token = token;
673         widget[id].value = value;
674     }
675     return id;
676 }
677
678 int gui_label(int pd, const char *text, int size, int rect, const float *c0,
679                                                             const float *c1)
680 {
681     int id;
682
683     if ((id = gui_widget(pd, GUI_LABEL)))
684     {
685         widget[id].text_img = make_image_from_font(NULL, NULL,
686                                                    &widget[id].w,
687                                                    &widget[id].h,
688                                                    text, font[size]);
689         widget[id].size   = size;
690         widget[id].color0 = c0 ? c0 : gui_yel;
691         widget[id].color1 = c1 ? c1 : gui_red;
692         widget[id].rect   = rect;
693     }
694     return id;
695 }
696
697 int gui_count(int pd, int value, int size, int rect)
698 {
699     int i, id;
700
701     if ((id = gui_widget(pd, GUI_COUNT)))
702     {
703         for (i = value; i; i /= 10)
704             widget[id].w += digit_w[size][0];
705
706         widget[id].h      = digit_h[size][0];
707         widget[id].value  = value;
708         widget[id].size   = size;
709         widget[id].color0 = gui_yel;
710         widget[id].color1 = gui_red;
711         widget[id].rect   = rect;
712     }
713     return id;
714 }
715
716 int gui_clock(int pd, int value, int size, int rect)
717 {
718     int id;
719
720     if ((id = gui_widget(pd, GUI_CLOCK)))
721     {
722         widget[id].w      = digit_w[size][0] * 6;
723         widget[id].h      = digit_h[size][0];
724         widget[id].value  = value;
725         widget[id].size   = size;
726         widget[id].color0 = gui_yel;
727         widget[id].color1 = gui_red;
728         widget[id].rect   = rect;
729     }
730     return id;
731 }
732
733 int gui_space(int pd)
734 {
735     int id;
736
737     if ((id = gui_widget(pd, GUI_SPACE)))
738     {
739         widget[id].w = 0;
740         widget[id].h = 0;
741     }
742     return id;
743 }
744
745 /*---------------------------------------------------------------------------*/
746 /*
747  * Create  a multi-line  text box  using a  vertical array  of labels.
748  * Parse the  text for '\'  characters and treat them  as line-breaks.
749  * Preserve the rect specification across the entire array.
750  */
751
752 int gui_multi(int pd, const char *text, int size, int rect, const float *c0,
753                                                             const float *c1)
754 {
755     int id = 0;
756
757     if (text && (id = gui_varray(pd)))
758     {
759         const char *p;
760
761         char s[8][MAXSTR];
762         int  r[8];
763         int  i, j;
764
765         size_t n = 0;
766
767         /* Copy each delimited string to a line buffer. */
768
769         for (p = text, j = 0; *p && j < 8; j++)
770         {
771             strncpy(s[j], p, (n = strcspn(p, "\\")));
772             s[j][n] = 0;
773             r[j]    = 0;
774
775             if (*(p += n) == '\\') p++;
776         }
777
778         /* Set the curves for the first and last lines. */
779
780         if (j > 0)
781         {
782             r[0]     |= rect & (GUI_NW | GUI_NE);
783             r[j - 1] |= rect & (GUI_SW | GUI_SE);
784         }
785
786         /* Create a label widget for each line. */
787
788         for (i = 0; i < j; i++)
789             gui_label(id, s[i], size, r[i], c0, c1);
790     }
791     return id;
792 }
793
794 /*---------------------------------------------------------------------------*/
795 /*
796  * The bottom-up pass determines the area of all widgets.  The minimum
797  * width  and height of  a leaf  widget is  given by  the size  of its
798  * contents.   Array  and  stack   widths  and  heights  are  computed
799  * recursively from these.
800  */
801
802 static void gui_widget_up(int id);
803
804 static void gui_harray_up(int id)
805 {
806     int jd, c = 0;
807
808     /* Find the widest child width and the highest child height. */
809
810     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
811     {
812         gui_widget_up(jd);
813
814         if (widget[id].h < widget[jd].h)
815             widget[id].h = widget[jd].h;
816         if (widget[id].w < widget[jd].w)
817             widget[id].w = widget[jd].w;
818
819         c++;
820     }
821
822     /* Total width is the widest child width times the child count. */
823
824     widget[id].w *= c;
825 }
826
827 static void gui_varray_up(int id)
828 {
829     int jd, c = 0;
830
831     /* Find the widest child width and the highest child height. */
832
833     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
834     {
835         gui_widget_up(jd);
836
837         if (widget[id].h < widget[jd].h)
838             widget[id].h = widget[jd].h;
839         if (widget[id].w < widget[jd].w)
840             widget[id].w = widget[jd].w;
841
842         c++;
843     }
844
845     /* Total height is the highest child height times the child count. */
846
847     widget[id].h *= c;
848 }
849
850 static void gui_hstack_up(int id)
851 {
852     int jd;
853
854     /* Find the highest child height.  Sum the child widths. */
855
856     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
857     {
858         gui_widget_up(jd);
859
860         if (widget[id].h < widget[jd].h)
861             widget[id].h = widget[jd].h;
862
863         widget[id].w += widget[jd].w;
864     }
865 }
866
867 static void gui_vstack_up(int id)
868 {
869     int jd;
870
871     /* Find the widest child width.  Sum the child heights. */
872
873     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
874     {
875         gui_widget_up(jd);
876
877         if (widget[id].w < widget[jd].w)
878             widget[id].w = widget[jd].w;
879
880         widget[id].h += widget[jd].h;
881     }
882 }
883
884 static void gui_button_up(int id)
885 {
886     /* Store width and height for later use in text rendering. */
887
888     widget[id].text_obj_w = widget[id].w;
889     widget[id].text_obj_h = widget[id].h;
890
891     if (widget[id].w < widget[id].h && widget[id].w > 0)
892         widget[id].w = widget[id].h;
893
894     /* Padded text elements look a little nicer. */
895
896     if (widget[id].w < config_get_d(CONFIG_WIDTH))
897         widget[id].w += radius;
898     if (widget[id].h < config_get_d(CONFIG_HEIGHT))
899         widget[id].h += radius;
900
901     /* A button should be at least wide enough to accomodate the rounding. */
902
903     if (widget[id].w < 2 * radius)
904         widget[id].w = 2 * radius;
905     if (widget[id].h < 2 * radius)
906         widget[id].h = 2 * radius;
907 }
908
909 static void gui_widget_up(int id)
910 {
911     if (id)
912         switch (widget[id].type & GUI_TYPE)
913         {
914         case GUI_HARRAY: gui_harray_up(id); break;
915         case GUI_VARRAY: gui_varray_up(id); break;
916         case GUI_HSTACK: gui_hstack_up(id); break;
917         case GUI_VSTACK: gui_vstack_up(id); break;
918         default:         gui_button_up(id); break;
919         }
920 }
921
922 /*---------------------------------------------------------------------------*/
923 /*
924  * The  top-down layout  pass distributes  available area  as computed
925  * during the bottom-up pass.  Widgets  use their area and position to
926  * initialize rendering state.
927  */
928
929 static void gui_widget_dn(int id, int x, int y, int w, int h);
930
931 static void gui_harray_dn(int id, int x, int y, int w, int h)
932 {
933     int jd, i = 0, c = 0;
934
935     widget[id].x = x;
936     widget[id].y = y;
937     widget[id].w = w;
938     widget[id].h = h;
939
940     /* Count children. */
941
942     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
943         c += 1;
944
945     /* Distribute horizontal space evenly to all children. */
946
947     for (jd = widget[id].car; jd; jd = widget[jd].cdr, i++)
948     {
949         int x0 = x +  i      * w / c;
950         int x1 = x + (i + 1) * w / c;
951
952         gui_widget_dn(jd, x0, y, x1 - x0, h);
953     }
954 }
955
956 static void gui_varray_dn(int id, int x, int y, int w, int h)
957 {
958     int jd, i = 0, c = 0;
959
960     widget[id].x = x;
961     widget[id].y = y;
962     widget[id].w = w;
963     widget[id].h = h;
964
965     /* Count children. */
966
967     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
968         c += 1;
969
970     /* Distribute vertical space evenly to all children. */
971
972     for (jd = widget[id].car; jd; jd = widget[jd].cdr, i++)
973     {
974         int y0 = y +  i      * h / c;
975         int y1 = y + (i + 1) * h / c;
976
977         gui_widget_dn(jd, x, y0, w, y1 - y0);
978     }
979 }
980
981 static void gui_hstack_dn(int id, int x, int y, int w, int h)
982 {
983     int jd, jx = x, jw = 0, c = 0;
984
985     widget[id].x = x;
986     widget[id].y = y;
987     widget[id].w = w;
988     widget[id].h = h;
989
990     /* Measure the total width requested by non-filler children. */
991
992     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
993         if ((widget[jd].type & GUI_TYPE) == GUI_FILLER)
994             c += 1;
995         else
996             jw += widget[jd].w;
997
998     /* Give non-filler children their requested space.   */
999     /* Distribute the rest evenly among filler children. */
1000
1001     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
1002     {
1003         if ((widget[jd].type & GUI_TYPE) == GUI_FILLER)
1004             gui_widget_dn(jd, jx, y, (w - jw) / c, h);
1005         else
1006             gui_widget_dn(jd, jx, y, widget[jd].w, h);
1007
1008         jx += widget[jd].w;
1009     }
1010 }
1011
1012 static void gui_vstack_dn(int id, int x, int y, int w, int h)
1013 {
1014     int jd, jy = y, jh = 0, c = 0;
1015
1016     widget[id].x = x;
1017     widget[id].y = y;
1018     widget[id].w = w;
1019     widget[id].h = h;
1020
1021     /* Measure the total height requested by non-filler children. */
1022
1023     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
1024         if ((widget[jd].type & GUI_TYPE) == GUI_FILLER)
1025             c += 1;
1026         else
1027             jh += widget[jd].h;
1028
1029     /* Give non-filler children their requested space.   */
1030     /* Distribute the rest evenly among filler children. */
1031
1032     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
1033     {
1034         if ((widget[jd].type & GUI_TYPE) == GUI_FILLER)
1035             gui_widget_dn(jd, x, jy, w, (h - jh) / c);
1036         else
1037             gui_widget_dn(jd, x, jy, w, widget[jd].h);
1038
1039         jy += widget[jd].h;
1040     }
1041 }
1042
1043 static void gui_filler_dn(int id, int x, int y, int w, int h)
1044 {
1045     /* Filler expands to whatever size it is given. */
1046
1047     widget[id].x = x;
1048     widget[id].y = y;
1049     widget[id].w = w;
1050     widget[id].h = h;
1051 }
1052
1053 static void gui_button_dn(int id, int x, int y, int w, int h)
1054 {
1055     /* Recall stored width and height for text rendering. */
1056
1057     int W = widget[id].text_obj_w;
1058     int H = widget[id].text_obj_h;
1059     int R = widget[id].rect;
1060
1061     const float *c0 = widget[id].color0;
1062     const float *c1 = widget[id].color1;
1063
1064     widget[id].x = x;
1065     widget[id].y = y;
1066     widget[id].w = w;
1067     widget[id].h = h;
1068
1069     /* Create display lists for the text area and rounded rectangle. */
1070
1071     widget[id].text_obj = gui_list(-W / 2, -H / 2, W, H, c0, c1);
1072     widget[id].rect_obj = gui_rect(-w / 2, -h / 2, w, h, R, radius);
1073 }
1074
1075 static void gui_widget_dn(int id, int x, int y, int w, int h)
1076 {
1077     if (id)
1078         switch (widget[id].type & GUI_TYPE)
1079         {
1080         case GUI_HARRAY: gui_harray_dn(id, x, y, w, h); break;
1081         case GUI_VARRAY: gui_varray_dn(id, x, y, w, h); break;
1082         case GUI_HSTACK: gui_hstack_dn(id, x, y, w, h); break;
1083         case GUI_VSTACK: gui_vstack_dn(id, x, y, w, h); break;
1084         case GUI_FILLER: gui_filler_dn(id, x, y, w, h); break;
1085         case GUI_SPACE:  gui_filler_dn(id, x, y, w, h); break;
1086         default:         gui_button_dn(id, x, y, w, h); break;
1087         }
1088 }
1089
1090 /*---------------------------------------------------------------------------*/
1091 /*
1092  * During GUI layout, we make a bottom-up pass to determine total area
1093  * requirements for  the widget  tree.  We position  this area  to the
1094  * sides or center of the screen.  Finally, we make a top-down pass to
1095  * distribute this area to each widget.
1096  */
1097
1098 void gui_layout(int id, int xd, int yd)
1099 {
1100     int x, y;
1101
1102     int w, W = config_get_d(CONFIG_WIDTH);
1103     int h, H = config_get_d(CONFIG_HEIGHT);
1104
1105     gui_widget_up(id);
1106
1107     w = widget[id].w;
1108     h = widget[id].h;
1109
1110     if      (xd < 0) x = 0;
1111     else if (xd > 0) x = (W - w);
1112     else             x = (W - w) / 2;
1113
1114     if      (yd < 0) y = 0;
1115     else if (yd > 0) y = (H - h);
1116     else             y = (H - h) / 2;
1117
1118     gui_widget_dn(id, x, y, w, h);
1119
1120     /* Hilite the widget under the cursor, if any. */
1121
1122     gui_point(id, -1, -1);
1123 }
1124
1125 int gui_search(int id, int x, int y)
1126 {
1127     int jd, kd;
1128
1129     /* Search the hierarchy for the widget containing the given point. */
1130
1131     if (id && (widget[id].x <= x && x < widget[id].x + widget[id].w &&
1132                widget[id].y <= y && y < widget[id].y + widget[id].h))
1133     {
1134         if (gui_hot(id))
1135             return id;
1136
1137         for (jd = widget[id].car; jd; jd = widget[jd].cdr)
1138             if ((kd = gui_search(jd, x, y)))
1139                 return kd;
1140     }
1141     return 0;
1142 }
1143
1144 /*
1145  * Activate a widget, allowing it  to behave as a normal state widget.
1146  * This may  be used  to create  image buttons, or  cause an  array of
1147  * widgets to behave as a single state widget.
1148  */
1149 int gui_active(int id, int token, int value)
1150 {
1151     widget[id].type |= GUI_STATE;
1152     widget[id].token = token;
1153     widget[id].value = value;
1154
1155     return id;
1156 }
1157
1158 int gui_delete(int id)
1159 {
1160     if (id)
1161     {
1162         /* Recursively delete all subwidgets. */
1163
1164         gui_delete(widget[id].cdr);
1165         gui_delete(widget[id].car);
1166
1167         /* Release any GL resources held by this widget. */
1168
1169         if (glIsTexture(widget[id].text_img))
1170             glDeleteTextures(1, &widget[id].text_img);
1171
1172         if (glIsList(widget[id].text_obj))
1173             glDeleteLists(widget[id].text_obj, 1);
1174         if (glIsList(widget[id].rect_obj))
1175             glDeleteLists(widget[id].rect_obj, 1);
1176
1177         /* Mark this widget unused. */
1178
1179         widget[id].type     = GUI_FREE;
1180         widget[id].text_img = 0;
1181         widget[id].text_obj = 0;
1182         widget[id].rect_obj = 0;
1183         widget[id].cdr      = 0;
1184         widget[id].car      = 0;
1185     }
1186     return 0;
1187 }
1188
1189 /*---------------------------------------------------------------------------*/
1190
1191 static void gui_paint_rect(int id, int st)
1192 {
1193     static const GLfloat back[4][4] = {
1194         { 0.1f, 0.1f, 0.1f, 0.5f },             /* off and inactive    */
1195         { 0.5f, 0.5f, 0.5f, 0.8f },             /* off and   active    */
1196         { 1.0f, 0.7f, 0.3f, 0.5f },             /* on  and inactive    */
1197         { 1.0f, 0.7f, 0.3f, 0.8f },             /* on  and   active    */
1198     };
1199
1200     int jd, i = 0;
1201
1202     /* Use the widget status to determine the background color. */
1203
1204     if (gui_hot(id))
1205         i = st | (((widget[id].value) ? 2 : 0) |
1206                   ((id == active)     ? 1 : 0));
1207
1208     switch (widget[id].type & GUI_TYPE)
1209     {
1210     case GUI_IMAGE:
1211     case GUI_SPACE:
1212     case GUI_FILLER:
1213         break;
1214
1215     case GUI_HARRAY:
1216     case GUI_VARRAY:
1217     case GUI_HSTACK:
1218     case GUI_VSTACK:
1219
1220         /* Recursively paint all subwidgets. */
1221
1222         for (jd = widget[id].car; jd; jd = widget[jd].cdr)
1223             gui_paint_rect(jd, i);
1224
1225         break;
1226
1227     default:
1228
1229         /* Draw a leaf's background, colored by widget state. */
1230
1231         glPushMatrix();
1232         {
1233             glTranslatef((GLfloat) (widget[id].x + widget[id].w / 2),
1234                          (GLfloat) (widget[id].y + widget[id].h / 2), 0.f);
1235
1236             glColor4fv(back[i]);
1237             glCallList(widget[id].rect_obj);
1238         }
1239         glPopMatrix();
1240
1241         break;
1242     }
1243 }
1244
1245 /*---------------------------------------------------------------------------*/
1246
1247 static void gui_paint_text(int id);
1248
1249 static void gui_paint_array(int id)
1250 {
1251     int jd;
1252
1253     glPushMatrix();
1254     {
1255         GLfloat cx = widget[id].x + widget[id].w / 2.0f;
1256         GLfloat cy = widget[id].y + widget[id].h / 2.0f;
1257         GLfloat ck = widget[id].scale;
1258
1259         glTranslatef(+cx, +cy, 0.0f);
1260         glScalef(ck, ck, ck);
1261         glTranslatef(-cx, -cy, 0.0f);
1262
1263         /* Recursively paint all subwidgets. */
1264
1265         for (jd = widget[id].car; jd; jd = widget[jd].cdr)
1266             gui_paint_text(jd);
1267     }
1268     glPopMatrix();
1269 }
1270
1271 static void gui_paint_image(int id)
1272 {
1273     /* Draw the widget rect, textured using the image. */
1274
1275     glPushMatrix();
1276     {
1277         glTranslatef((GLfloat) (widget[id].x + widget[id].w / 2),
1278                      (GLfloat) (widget[id].y + widget[id].h / 2), 0.f);
1279
1280         glScalef(widget[id].scale,
1281                  widget[id].scale,
1282                  widget[id].scale);
1283
1284         glBindTexture(GL_TEXTURE_2D, widget[id].text_img);
1285         glColor4fv(gui_wht);
1286         glCallList(widget[id].rect_obj);
1287     }
1288     glPopMatrix();
1289 }
1290
1291 static void gui_paint_count(int id)
1292 {
1293     int j, i = widget[id].size;
1294
1295     glPushMatrix();
1296     {
1297         glColor4fv(gui_wht);
1298
1299         /* Translate to the widget center, and apply the pulse scale. */
1300
1301         glTranslatef((GLfloat) (widget[id].x + widget[id].w / 2),
1302                      (GLfloat) (widget[id].y + widget[id].h / 2), 0.f);
1303
1304         glScalef(widget[id].scale,
1305                  widget[id].scale,
1306                  widget[id].scale);
1307
1308         if (widget[id].value > 0)
1309         {
1310             /* Translate left by half the total width of the rendered value. */
1311
1312             for (j = widget[id].value; j; j /= 10)
1313                 glTranslatef((GLfloat) +digit_w[i][j % 10] / 2.0f, 0.0f, 0.0f);
1314
1315             glTranslatef((GLfloat) -digit_w[i][0] / 2.0f, 0.0f, 0.0f);
1316
1317             /* Render each digit, moving right after each. */
1318
1319             for (j = widget[id].value; j; j /= 10)
1320             {
1321                 glBindTexture(GL_TEXTURE_2D, digit_text[i][j % 10]);
1322                 glCallList(digit_list[i][j % 10]);
1323                 glTranslatef((GLfloat) -digit_w[i][j % 10], 0.0f, 0.0f);
1324             }
1325         }
1326         else if (widget[id].value == 0)
1327         {
1328             /* If the value is zero, just display a zero in place. */
1329
1330             glBindTexture(GL_TEXTURE_2D, digit_text[i][0]);
1331             glCallList(digit_list[i][0]);
1332         }
1333     }
1334     glPopMatrix();
1335 }
1336
1337 static void gui_paint_clock(int id)
1338 {
1339     int i  =   widget[id].size;
1340     int mt =  (widget[id].value / 6000) / 10;
1341     int mo =  (widget[id].value / 6000) % 10;
1342     int st = ((widget[id].value % 6000) / 100) / 10;
1343     int so = ((widget[id].value % 6000) / 100) % 10;
1344     int ht = ((widget[id].value % 6000) % 100) / 10;
1345     int ho = ((widget[id].value % 6000) % 100) % 10;
1346
1347     GLfloat dx_large = (GLfloat) digit_w[i][0];
1348     GLfloat dx_small = (GLfloat) digit_w[i][0] * 0.75f;
1349
1350     if (widget[id].value < 0)
1351         return;
1352
1353     glPushMatrix();
1354     {
1355         glColor4fv(gui_wht);
1356
1357         /* Translate to the widget center, and apply the pulse scale. */
1358
1359         glTranslatef((GLfloat) (widget[id].x + widget[id].w / 2),
1360                      (GLfloat) (widget[id].y + widget[id].h / 2), 0.f);
1361
1362         glScalef(widget[id].scale,
1363                  widget[id].scale,
1364                  widget[id].scale);
1365
1366         /* Translate left by half the total width of the rendered value. */
1367
1368         if (mt > 0)
1369             glTranslatef(-2.25f * dx_large, 0.0f, 0.0f);
1370         else
1371             glTranslatef(-1.75f * dx_large, 0.0f, 0.0f);
1372
1373         /* Render the minutes counter. */
1374
1375         if (mt > 0)
1376         {
1377             glBindTexture(GL_TEXTURE_2D, digit_text[i][mt]);
1378             glCallList(digit_list[i][mt]);
1379             glTranslatef(dx_large, 0.0f, 0.0f);
1380         }
1381
1382         glBindTexture(GL_TEXTURE_2D, digit_text[i][mo]);
1383         glCallList(digit_list[i][mo]);
1384         glTranslatef(dx_small, 0.0f, 0.0f);
1385
1386         /* Render the colon. */
1387
1388         glBindTexture(GL_TEXTURE_2D, digit_text[i][10]);
1389         glCallList(digit_list[i][10]);
1390         glTranslatef(dx_small, 0.0f, 0.0f);
1391
1392         /* Render the seconds counter. */
1393
1394         glBindTexture(GL_TEXTURE_2D, digit_text[i][st]);
1395         glCallList(digit_list[i][st]);
1396         glTranslatef(dx_large, 0.0f, 0.0f);
1397
1398         glBindTexture(GL_TEXTURE_2D, digit_text[i][so]);
1399         glCallList(digit_list[i][so]);
1400         glTranslatef(dx_small, 0.0f, 0.0f);
1401
1402         /* Render hundredths counter half size. */
1403
1404         glScalef(0.5f, 0.5f, 1.0f);
1405
1406         glBindTexture(GL_TEXTURE_2D, digit_text[i][ht]);
1407         glCallList(digit_list[i][ht]);
1408         glTranslatef(dx_large, 0.0f, 0.0f);
1409
1410         glBindTexture(GL_TEXTURE_2D, digit_text[i][ho]);
1411         glCallList(digit_list[i][ho]);
1412     }
1413     glPopMatrix();
1414 }
1415
1416 static void gui_paint_label(int id)
1417 {
1418     /* Draw the widget text box, textured using the glyph. */
1419
1420     glPushMatrix();
1421     {
1422         glTranslatef((GLfloat) (widget[id].x + widget[id].w / 2),
1423                      (GLfloat) (widget[id].y + widget[id].h / 2), 0.f);
1424
1425         glScalef(widget[id].scale,
1426                  widget[id].scale,
1427                  widget[id].scale);
1428
1429         glBindTexture(GL_TEXTURE_2D, widget[id].text_img);
1430         glCallList(widget[id].text_obj);
1431     }
1432     glPopMatrix();
1433 }
1434
1435 static void gui_paint_text(int id)
1436 {
1437     switch (widget[id].type & GUI_TYPE)
1438     {
1439     case GUI_SPACE:  break;
1440     case GUI_FILLER: break;
1441     case GUI_HARRAY: gui_paint_array(id); break;
1442     case GUI_VARRAY: gui_paint_array(id); break;
1443     case GUI_HSTACK: gui_paint_array(id); break;
1444     case GUI_VSTACK: gui_paint_array(id); break;
1445     case GUI_IMAGE:  gui_paint_image(id); break;
1446     case GUI_COUNT:  gui_paint_count(id); break;
1447     case GUI_CLOCK:  gui_paint_clock(id); break;
1448     default:         gui_paint_label(id); break;
1449     }
1450 }
1451
1452 void gui_paint(int id)
1453 {
1454     if (id)
1455     {
1456         video_push_ortho();
1457         {
1458             glEnable(GL_COLOR_MATERIAL);
1459             glDisable(GL_LIGHTING);
1460             glDisable(GL_DEPTH_TEST);
1461             {
1462                 glDisable(GL_TEXTURE_2D);
1463                 gui_paint_rect(id, 0);
1464
1465                 glEnable(GL_TEXTURE_2D);
1466                 gui_paint_text(id);
1467
1468                 glColor4fv(gui_wht);
1469             }
1470             glEnable(GL_DEPTH_TEST);
1471             glEnable(GL_LIGHTING);
1472             glDisable(GL_COLOR_MATERIAL);
1473         }
1474         video_pop_matrix();
1475     }
1476 }
1477
1478 /*---------------------------------------------------------------------------*/
1479
1480 void gui_dump(int id, int d)
1481 {
1482     int jd, i;
1483
1484     if (id)
1485     {
1486         char *type = "?";
1487
1488         switch (widget[id].type & GUI_TYPE)
1489         {
1490         case GUI_HARRAY: type = "harray"; break;
1491         case GUI_VARRAY: type = "varray"; break;
1492         case GUI_HSTACK: type = "hstack"; break;
1493         case GUI_VSTACK: type = "vstack"; break;
1494         case GUI_FILLER: type = "filler"; break;
1495         case GUI_IMAGE:  type = "image";  break;
1496         case GUI_LABEL:  type = "label";  break;
1497         case GUI_COUNT:  type = "count";  break;
1498         case GUI_CLOCK:  type = "clock";  break;
1499         }
1500
1501         for (i = 0; i < d; i++)
1502             printf("    ");
1503
1504         printf("%04d %s\n", id, type);
1505
1506         for (jd = widget[id].car; jd; jd = widget[jd].cdr)
1507             gui_dump(jd, d + 1);
1508     }
1509 }
1510
1511 void gui_pulse(int id, float k)
1512 {
1513     if (id) widget[id].scale = k;
1514 }
1515
1516 void gui_timer(int id, float dt)
1517 {
1518     int jd;
1519
1520     if (id)
1521     {
1522         for (jd = widget[id].car; jd; jd = widget[jd].cdr)
1523             gui_timer(jd, dt);
1524
1525         if (widget[id].scale - 1.0f < dt)
1526             widget[id].scale = 1.0f;
1527         else
1528             widget[id].scale -= dt;
1529     }
1530 }
1531
1532 int gui_point(int id, int x, int y)
1533 {
1534     static int x_cache = 0;
1535     static int y_cache = 0;
1536
1537     int jd;
1538
1539     /* Reuse the last coordinates if (x,y) == (-1,-1) */
1540
1541     if (x < 0 && y < 0)
1542         return gui_point(id, x_cache, y_cache);
1543
1544     x_cache = x;
1545     y_cache = y;
1546
1547     /* Short-circuit check the current active widget. */
1548
1549     jd = gui_search(active, x, y);
1550
1551     /* If not still active, search the hierarchy for a new active widget. */
1552
1553     if (jd == 0)
1554         jd = gui_search(id, x, y);
1555
1556     /* If the active widget has changed, return the new active id. */
1557
1558     if (jd == 0 || jd == active)
1559         return 0;
1560     else
1561         return active = jd;
1562 }
1563
1564 void gui_focus(int i)
1565 {
1566     active = i;
1567 }
1568
1569 int gui_click(void)
1570 {
1571     return active;
1572 }
1573
1574 int gui_token(int id)
1575 {
1576     return id ? widget[id].token : 0;
1577 }
1578
1579 int gui_value(int id)
1580 {
1581     return id ? widget[id].value : 0;
1582 }
1583
1584 void gui_toggle(int id)
1585 {
1586     widget[id].value = widget[id].value ? 0 : 1;
1587 }
1588
1589 /*---------------------------------------------------------------------------*/
1590
1591 static int gui_vert_offset(int id, int jd)
1592 {
1593     /* Vertical offset between bottom of id and top of jd */
1594
1595     return  widget[id].y - (widget[jd].y + widget[jd].h);
1596 }
1597
1598 static int gui_horz_offset(int id, int jd)
1599 {
1600     /* Horizontal offset between left of id and right of jd */
1601
1602     return  widget[id].x - (widget[jd].x + widget[jd].w);
1603 }
1604
1605 static int gui_vert_dist(int id, int jd)
1606 {
1607     /* Vertical distance between the tops of id and jd */
1608
1609     return abs((widget[id].y + widget[id].h) - (widget[jd].y + widget[jd].h));
1610 }
1611
1612 static int gui_horz_dist(int id, int jd)
1613 {
1614     /* Horizontal distance between the left sides of id and jd */
1615
1616     return abs(widget[id].x - widget[jd].x);
1617 }
1618
1619 /*---------------------------------------------------------------------------*/
1620
1621 static int gui_stick_L(int id, int dd)
1622 {
1623     int jd, kd, hd;
1624     int o, omin, d, dmin;
1625
1626     /* Find the closest "hot" widget to the left of dd (and inside id) */
1627
1628     if (id && gui_hot(id))
1629         return id;
1630
1631     hd = 0;
1632     omin = widget[dd].x - widget[id].x + 1;
1633     dmin = widget[dd].y + widget[dd].h + widget[id].y + widget[id].h;
1634
1635     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
1636     {
1637         kd = gui_stick_L(jd, dd);
1638
1639         if (kd && kd != dd)
1640         {
1641             o = gui_horz_offset(dd, kd);
1642             d = gui_vert_dist(dd, kd);
1643
1644             if (0 <= o && o <= omin && d <= dmin)
1645             {
1646                 hd = kd;
1647                 omin = o;
1648                 dmin = d;
1649             }
1650         }
1651     }
1652
1653     return hd;
1654 }
1655
1656 static int gui_stick_R(int id, int dd)
1657 {
1658     int jd, kd, hd;
1659     int o, omin, d, dmin;
1660
1661     /* Find the closest "hot" widget to the right of dd (and inside id) */
1662
1663     if (id && gui_hot(id))
1664         return id;
1665
1666     hd = 0;
1667     omin = (widget[id].x + widget[id].w) - (widget[dd].x + widget[dd].w) + 1;
1668     dmin = widget[dd].y + widget[dd].h + widget[id].y + widget[id].h;
1669
1670     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
1671     {
1672         kd = gui_stick_R(jd, dd);
1673
1674         if (kd && kd != dd)
1675         {
1676             o = gui_horz_offset(kd, dd);
1677             d = gui_vert_dist(dd, kd);
1678
1679             if (0 <= o && o <= omin && d <= dmin)
1680             {
1681                 hd = kd;
1682                 omin = o;
1683                 dmin = d;
1684             }
1685         }
1686     }
1687
1688     return hd;
1689 }
1690
1691 static int gui_stick_D(int id, int dd)
1692 {
1693     int jd, kd, hd;
1694     int o, omin, d, dmin;
1695
1696     /* Find the closest "hot" widget below dd (and inside id) */
1697
1698     if (id && gui_hot(id))
1699         return id;
1700
1701     hd = 0;
1702     omin = widget[dd].y - widget[id].y + 1;
1703     dmin = widget[dd].x + widget[dd].w + widget[id].x + widget[id].w;
1704
1705     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
1706     {
1707         kd = gui_stick_D(jd, dd);
1708
1709         if (kd && kd != dd)
1710         {
1711             o = gui_vert_offset(dd, kd);
1712             d = gui_horz_dist(dd, kd);
1713
1714             if (0 <= o && o <= omin && d <= dmin)
1715             {
1716                 hd = kd;
1717                 omin = o;
1718                 dmin = d;
1719             }
1720         }
1721     }
1722
1723     return hd;
1724 }
1725
1726 static int gui_stick_U(int id, int dd)
1727 {
1728     int jd, kd, hd;
1729     int o, omin, d, dmin;
1730
1731     /* Find the closest "hot" widget above dd (and inside id) */
1732
1733     if (id && gui_hot(id))
1734         return id;
1735
1736     hd = 0;
1737     omin = (widget[id].y + widget[id].h) - (widget[dd].y + widget[dd].h) + 1;
1738     dmin = widget[dd].x + widget[dd].w + widget[id].x + widget[id].w;
1739
1740     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
1741     {
1742         kd = gui_stick_U(jd, dd);
1743
1744         if (kd && kd != dd)
1745         {
1746             o = gui_vert_offset(kd, dd);
1747             d = gui_horz_dist(dd, kd);
1748
1749             if (0 <= o && o <= omin && d <= dmin)
1750             {
1751                 hd = kd;
1752                 omin = o;
1753                 dmin = d;
1754             }
1755         }
1756     }
1757
1758     return hd;
1759 }
1760
1761 /*---------------------------------------------------------------------------*/
1762
1763 static int gui_wrap_L(int id, int dd)
1764 {
1765     int jd, kd;
1766
1767     if ((jd = gui_stick_L(id, dd)) == 0)
1768         for (jd = dd; (kd = gui_stick_R(id, jd)); jd = kd)
1769             ;
1770
1771     return jd;
1772 }
1773
1774 static int gui_wrap_R(int id, int dd)
1775 {
1776     int jd, kd;
1777
1778     if ((jd = gui_stick_R(id, dd)) == 0)
1779         for (jd = dd; (kd = gui_stick_L(id, jd)); jd = kd)
1780             ;
1781
1782     return jd;
1783 }
1784
1785 static int gui_wrap_U(int id, int dd)
1786 {
1787     int jd, kd;
1788
1789     if ((jd = gui_stick_U(id, dd)) == 0)
1790         for (jd = dd; (kd = gui_stick_D(id, jd)); jd = kd)
1791             ;
1792
1793     return jd;
1794 }
1795
1796 static int gui_wrap_D(int id, int dd)
1797 {
1798     int jd, kd;
1799
1800     if ((jd = gui_stick_D(id, dd)) == 0)
1801         for (jd = dd; (kd = gui_stick_U(id, jd)); jd = kd)
1802             ;
1803
1804     return jd;
1805 }
1806
1807 /*---------------------------------------------------------------------------*/
1808
1809 /* Flag the axes to prevent uncontrolled scrolling. */
1810
1811 static int xflag = 1;
1812 static int yflag = 1;
1813
1814 void gui_stuck()
1815 {
1816     /* Force the user to recenter the joystick before the next GUI action. */
1817
1818     xflag = 0;
1819     yflag = 0;
1820 }
1821
1822 int gui_stick(int id, int x, int y)
1823 {
1824     int jd = 0;
1825
1826     /* Find a new active widget in the direction of joystick motion. */
1827
1828     if (x && -JOY_MID <= x && x <= +JOY_MID)
1829         xflag = 1;
1830     else if (x < -JOY_MID && xflag && (jd = gui_wrap_L(id, active)))
1831         xflag = 0;
1832     else if (x > +JOY_MID && xflag && (jd = gui_wrap_R(id, active)))
1833         xflag = 0;
1834
1835     if (y && -JOY_MID <= y && y <= +JOY_MID)
1836         yflag = 1;
1837     else if (y < -JOY_MID && yflag && (jd = gui_wrap_U(id, active)))
1838         yflag = 0;
1839     else if (y > +JOY_MID && yflag && (jd = gui_wrap_D(id, active)))
1840         yflag = 0;
1841
1842     /* If the active widget has changed, return the new active id. */
1843
1844     if (jd == 0 || jd == active)
1845         return 0;
1846     else
1847         return active = jd;
1848 }
1849
1850 /*---------------------------------------------------------------------------*/