Do not reject 1.5 format SOLs
[neverball] / share / gui.c
index b30c561..9da7434 100644 (file)
 #include <stdio.h>
 
 #include "config.h"
+#include "video.h"
 #include "glext.h"
 #include "image.h"
 #include "vec3.h"
 #include "gui.h"
+#include "common.h"
+
+#include "fs.h"
+#include "fs_rwops.h"
 
 /*---------------------------------------------------------------------------*/
 
@@ -40,7 +45,6 @@
 #define GUI_COUNT  16
 #define GUI_CLOCK  18
 #define GUI_SPACE  20
-#define GUI_PAUSE  22
 
 struct widget
 {
@@ -63,6 +67,11 @@ struct widget
     const GLfloat *color1;
 
     GLfloat  scale;
+
+    int text_obj_w;
+    int text_obj_h;
+
+    enum trunc trunc;
 };
 
 /*---------------------------------------------------------------------------*/
@@ -81,15 +90,16 @@ static struct widget widget[MAXWIDGET];
 static int           active;
 static int           radius;
 static TTF_Font     *font[3] = { NULL, NULL, NULL };
-static int           scale[3] = { 1, 1, 1 };
+
+static void      *fontdata;
+static int        fontdatalen;
+static SDL_RWops *fontrwops;
 
 static GLuint digit_text[3][11];
 static GLuint digit_list[3][11];
 static int    digit_w[3][11];
 static int    digit_h[3][11];
 
-static int pause_id;
-
 /*---------------------------------------------------------------------------*/
 
 static int gui_hot(int id)
@@ -101,7 +111,7 @@ static int gui_hot(int id)
 /*
  * Initialize a  display list  containing a  rectangle (x, y, w, h) to
  * which a  rendered-font texture  may be applied.   Colors  c0 and c1
- * determine the top-to-bottom color gradiant of the text.
+ * determine the top-to-bottom color gradient of the text.
  */
 
 static GLuint gui_list(int x, int y,
@@ -112,14 +122,17 @@ static GLuint gui_list(int x, int y,
     GLfloat s0, t0;
     GLfloat s1, t1;
 
-    int W, H, d = h / 16;
+    int W, H, ww, hh, d = h / 16;
 
     /* Assume the applied texture size is rect size rounded to power-of-two. */
 
     image_size(&W, &H, w, h);
 
-    s0 = 0.5f * (W - w) / W;
-    t0 = 0.5f * (H - h) / H;
+    ww = ((W - w) % 2) ? w + 1 : w;
+    hh = ((H - h) % 2) ? h + 1 : h;
+
+    s0 = 0.5f * (W - ww) / W;
+    t0 = 0.5f * (H - hh) / H;
     s1 = 1.0f - s0;
     t1 = 1.0f - t0;
 
@@ -128,18 +141,18 @@ static GLuint gui_list(int x, int y,
         glBegin(GL_QUADS);
         {
             glColor4f(0.0f, 0.0f, 0.0f, 0.5f);
-            glTexCoord2f(s0, t1); glVertex2i(x     + d, y     - d);
-            glTexCoord2f(s1, t1); glVertex2i(x + w + d, y     - d);
-            glTexCoord2f(s1, t0); glVertex2i(x + w + d, y + h - d);
-            glTexCoord2f(s0, t0); glVertex2i(x     + d, y + h - d);
+            glTexCoord2f(s0, t1); glVertex2i(x      + d, y      - d);
+            glTexCoord2f(s1, t1); glVertex2i(x + ww + d, y      - d);
+            glTexCoord2f(s1, t0); glVertex2i(x + ww + d, y + hh - d);
+            glTexCoord2f(s0, t0); glVertex2i(x      + d, y + hh - d);
 
             glColor4fv(c0);
-            glTexCoord2f(s0, t1); glVertex2i(x,     y);
-            glTexCoord2f(s1, t1); glVertex2i(x + w, y);
+            glTexCoord2f(s0, t1); glVertex2i(x,      y);
+            glTexCoord2f(s1, t1); glVertex2i(x + ww, y);
 
             glColor4fv(c1);
-            glTexCoord2f(s1, t0); glVertex2i(x + w, y + h);
-            glTexCoord2f(s0, t0); glVertex2i(x,     y + h);
+            glTexCoord2f(s1, t0); glVertex2i(x + ww, y + hh);
+            glTexCoord2f(s0, t0); glVertex2i(x,      y + hh);
         }
         glEnd();
     }
@@ -177,10 +190,10 @@ static GLuint gui_rect(int x, int y, int w, int h, int f, int r)
                 float Ya = y + h + ((f & GUI_NW) ? (s - r) : 0);
                 float Yb = y     + ((f & GUI_SW) ? (r - s) : 0);
 
-                glTexCoord2f((X - x) / w, 1 - (Ya - y) / h);
+                glTexCoord2f((X - x) / w, (Ya - y) / h);
                 glVertex2f(X, Ya);
 
-                glTexCoord2f((X - x) / w, 1 - (Yb - y) / h);
+                glTexCoord2f((X - x) / w, (Yb - y) / h);
                 glVertex2f(X, Yb);
             }
 
@@ -196,10 +209,10 @@ static GLuint gui_rect(int x, int y, int w, int h, int f, int r)
                 float Ya = y + h + ((f & GUI_NE) ? (c - r) : 0);
                 float Yb = y     + ((f & GUI_SE) ? (r - c) : 0);
 
-                glTexCoord2f((X - x) / w, 1 - (Ya - y) / h);
+                glTexCoord2f((X - x) / w, (Ya - y) / h);
                 glVertex2f(X, Ya);
 
-                glTexCoord2f((X - x) / w, 1 - (Yb - y) / h);
+                glTexCoord2f((X - x) / w, (Yb - y) / h);
                 glVertex2f(X, Yb);
             }
         }
@@ -212,6 +225,23 @@ static GLuint gui_rect(int x, int y, int w, int h, int f, int r)
 
 /*---------------------------------------------------------------------------*/
 
+static const char *pick_font_path(void)
+{
+    const char *path;
+
+    path = _(GUI_FACE);
+
+    if (!fs_exists(path))
+    {
+        fprintf(stderr, _("Font '%s' doesn't exist, trying default font.\n"),
+                path);
+
+        path = GUI_FACE;
+    }
+
+    return path;
+}
+
 void gui_init(void)
 {
     const float *c0 = gui_yel;
@@ -225,36 +255,38 @@ void gui_init(void)
 
     if (TTF_Init() == 0)
     {
-        int s0 = s / 26;
-        int s1 = s / 12;
-        int s2 = s /  6;
-        int m;
+        const char *fontpath = pick_font_path();
 
-        /* Make sure text size doesn't exceed the maximum texture size. */
+        int s0 = s / 26;
+        int s1 = s / 13;
+        int s2 = s /  7;
 
-        glGetIntegerv(GL_MAX_TEXTURE_SIZE, &m);
+        memset(widget, 0, sizeof (struct widget) * MAXWIDGET);
 
-        scale[0] = 1;
-        scale[1] = 1;
-        scale[2] = 1;
+        /* Load the font. */
 
-        while (s0 > m) { s0 /= 2; scale[0] *= 2; }
-        while (s1 > m) { s1 /= 2; scale[1] *= 2; }
-        while (s2 > m) { s2 /= 2; scale[2] *= 2; }
+        if (!(fontdata = fs_load(fontpath, &fontdatalen)))
+        {
+            fprintf(stderr, _("Could not load font '%s'.\n"), fontpath);
+            /* Return or no return, we'll probably crash now. */
+            return;
+        }
 
-        memset(widget, 0, sizeof (struct widget) * MAXWIDGET);
+        fontrwops = SDL_RWFromConstMem(fontdata, fontdatalen);
 
         /* Load small, medium, and large typefaces. */
 
-        font[GUI_SML] = TTF_OpenFont(config_data(GUI_FACE), s0);
-        font[GUI_MED] = TTF_OpenFont(config_data(GUI_FACE), s1);
-        font[GUI_LRG] = TTF_OpenFont(config_data(GUI_FACE), s2);
-        radius = s / 60;
+        font[GUI_SML] = TTF_OpenFontRW(fontrwops, 0, s0);
+
+        SDL_RWseek(fontrwops, 0, SEEK_SET);
+        font[GUI_MED] = TTF_OpenFontRW(fontrwops, 0, s1);
+
+        SDL_RWseek(fontrwops, 0, SEEK_SET);
+        font[GUI_LRG] = TTF_OpenFontRW(fontrwops, 0, s2);
 
-        /* Initialize the global pause GUI. */
+        /* fontrwops remains open. */
 
-        if ((pause_id = gui_pause(0)))
-            gui_layout(pause_id, 0, 0);
+        radius = s / 60;
 
         /* Initialize digit glyphs and lists for counters and clocks. */
 
@@ -262,7 +294,7 @@ void gui_init(void)
         {
             char text[2];
 
-            /* Draw digits 0 throught 9. */
+            /* Draw digits 0 through 9. */
 
             for (j = 0; j < 10; j++)
             {
@@ -272,7 +304,7 @@ void gui_init(void)
                 digit_text[i][j] = make_image_from_font(NULL, NULL,
                                                         &digit_w[i][j],
                                                         &digit_h[i][j],
-                                                        text, font[i], scale[i]);
+                                                        text, font[i]);
                 digit_list[i][j] = gui_list(-digit_w[i][j] / 2,
                                             -digit_h[i][j] / 2,
                                             +digit_w[i][j],
@@ -284,7 +316,7 @@ void gui_init(void)
             digit_text[i][j] = make_image_from_font(NULL, NULL,
                                                     &digit_w[i][10],
                                                     &digit_h[i][10],
-                                                    ":", font[i], scale[i]);
+                                                    ":", font[i]);
             digit_list[i][j] = gui_list(-digit_w[i][10] / 2,
                                         -digit_h[i][10] / 2,
                                         +digit_w[i][10],
@@ -337,6 +369,9 @@ void gui_free(void)
     if (font[GUI_MED]) TTF_CloseFont(font[GUI_MED]);
     if (font[GUI_SML]) TTF_CloseFont(font[GUI_SML]);
 
+    if (fontrwops) SDL_RWclose(fontrwops);
+    if (fontdata)  free(fontdata);
+
     TTF_Quit();
 }
 
@@ -366,8 +401,12 @@ static int gui_widget(int pd, int type)
             widget[id].color0   = gui_wht;
             widget[id].color1   = gui_wht;
             widget[id].scale    = 1.0f;
+            widget[id].trunc    = TRUNC_NONE;
+
+            widget[id].text_obj_w = 0;
+            widget[id].text_obj_h = 0;
 
-            /* Insert the new widget into the parents's widget list. */
+            /* Insert the new widget into the parent's widget list. */
 
             if (pd)
             {
@@ -397,12 +436,107 @@ int gui_filler(int pd) { return gui_widget(pd, GUI_FILLER); }
 
 /*---------------------------------------------------------------------------*/
 
+struct size
+{
+    int w, h;
+};
+
+
+static struct size gui_measure(const char *text, TTF_Font *font)
+{
+    struct size size = { 0, 0 };
+    TTF_SizeUTF8(font, text, &size.w, &size.h);
+    return size;
+}
+
+static char *gui_trunc_head(const char *text,
+                            const int maxwidth,
+                            TTF_Font *font)
+{
+    int left, right, mid;
+    char *str = NULL;
+
+    left  = 0;
+    right = strlen(text);
+
+    while (right - left > 1)
+    {
+        mid = (left + right) / 2;
+
+        str = concat_string("...", text + mid, NULL);
+
+        if (gui_measure(str, font).w <= maxwidth)
+            right = mid;
+        else
+            left = mid;
+
+        free(str);
+    }
+
+    return concat_string("...", text + right, NULL);
+}
+
+static char *gui_trunc_tail(const char *text,
+                            const int maxwidth,
+                            TTF_Font *font)
+{
+    int left, right, mid;
+    char *str = NULL;
+
+    left  = 0;
+    right = strlen(text);
+
+    while (right - left > 1)
+    {
+        mid = (left + right) / 2;
+
+        str = malloc(mid + sizeof ("..."));
+
+        memcpy(str,       text,  mid);
+        memcpy(str + mid, "...", sizeof ("..."));
+
+        if (gui_measure(str, font).w <= maxwidth)
+            left = mid;
+        else
+            right = mid;
+
+        free(str);
+    }
+
+    str = malloc(left + sizeof ("..."));
+
+    memcpy(str,        text,  left);
+    memcpy(str + left, "...", sizeof ("..."));
+
+    return str;
+}
+
+static char *gui_truncate(const char *text,
+                          const int maxwidth,
+                          TTF_Font *font,
+                          enum trunc trunc)
+{
+    if (gui_measure(text, font).w <= maxwidth)
+        return strdup(text);
+
+    switch (trunc)
+    {
+    case TRUNC_NONE: return strdup(text);                         break;
+    case TRUNC_HEAD: return gui_trunc_head(text, maxwidth, font); break;
+    case TRUNC_TAIL: return gui_trunc_tail(text, maxwidth, font); break;
+    }
+
+    return NULL;
+}
+
+/*---------------------------------------------------------------------------*/
+
 void gui_set_image(int id, const char *file)
 {
     if (glIsTexture(widget[id].text_img))
         glDeleteTextures(1, &widget[id].text_img);
 
-    widget[id].text_img = make_image_from_file(NULL, NULL, NULL, NULL, file);
+    widget[id].text_img = make_image_from_file(file);
 }
 
 void gui_set_label(int id, const char *text)
@@ -414,11 +548,19 @@ void gui_set_label(int id, const char *text)
     if (glIsList(widget[id].text_obj))
         glDeleteLists(widget[id].text_obj, 1);
 
+    text = gui_truncate(text, widget[id].w - radius,
+                        font[widget[id].size],
+                        widget[id].trunc);
+
     widget[id].text_img = make_image_from_font(NULL, NULL, &w, &h,
-                                               text, font[widget[id].size],
-                                                    scale[widget[id].size]);
+                                               text, font[widget[id].size]);
     widget[id].text_obj = gui_list(-w / 2, -h / 2, w, h,
                                    widget[id].color0, widget[id].color1);
+
+    widget[id].text_obj_w = w;
+    widget[id].text_obj_h = h;
+
+    free((void *) text);
 }
 
 void gui_set_count(int id, int value)
@@ -434,8 +576,31 @@ void gui_set_clock(int id, int value)
 void gui_set_color(int id, const float *c0,
                            const float *c1)
 {
-    widget[id].color0 = c0 ? c0 : gui_yel;
-    widget[id].color1 = c1 ? c1 : gui_red;
+    if (id)
+    {
+        c0 = c0 ? c0 : gui_yel;
+        c1 = c1 ? c1 : gui_red;
+
+        if (widget[id].color0 != c0 || widget[id].color1 != c1)
+        {
+            widget[id].color0 = c0;
+            widget[id].color1 = c1;
+
+            if (glIsList(widget[id].text_obj))
+            {
+                int w, h;
+
+                glDeleteLists(widget[id].text_obj, 1);
+
+                w = widget[id].text_obj_w;
+                h = widget[id].text_obj_h;
+
+                widget[id].text_obj = gui_list(-w / 2, -h / 2, w, h,
+                                               widget[id].color0,
+                                               widget[id].color1);
+            }
+        }
+    }
 }
 
 void gui_set_multi(int id, const char *text)
@@ -463,6 +628,11 @@ void gui_set_multi(int id, const char *text)
         gui_set_label(jd, s[i]);
 }
 
+void gui_set_trunc(int id, enum trunc trunc)
+{
+    widget[id].trunc = trunc;
+}
+
 /*---------------------------------------------------------------------------*/
 
 int gui_image(int pd, const char *file, int w, int h)
@@ -471,10 +641,9 @@ int gui_image(int pd, const char *file, int w, int h)
 
     if ((id = gui_widget(pd, GUI_IMAGE)))
     {
-        widget[id].text_img = make_image_from_file(NULL, NULL,
-                                                   NULL, NULL, file);
-        widget[id].w     = w;
-        widget[id].h     = h;
+        widget[id].text_img = make_image_from_file(file);
+        widget[id].w = w;
+        widget[id].h = h;
     }
     return id;
 }
@@ -498,8 +667,7 @@ int gui_state(int pd, const char *text, int size, int token, int value)
         widget[id].text_img = make_image_from_font(NULL, NULL,
                                                    &widget[id].w,
                                                    &widget[id].h,
-                                                   text, font[size],
-                                                        scale[size]);
+                                                   text, font[size]);
         widget[id].size  = size;
         widget[id].token = token;
         widget[id].value = value;
@@ -517,8 +685,7 @@ int gui_label(int pd, const char *text, int size, int rect, const float *c0,
         widget[id].text_img = make_image_from_font(NULL, NULL,
                                                    &widget[id].w,
                                                    &widget[id].h,
-                                                   text, font[size],
-                                                        scale[size]);
+                                                   text, font[size]);
         widget[id].size   = size;
         widget[id].color0 = c0 ? c0 : gui_yel;
         widget[id].color1 = c1 ? c1 : gui_red;
@@ -575,32 +742,11 @@ int gui_space(int pd)
     return id;
 }
 
-int gui_pause(int pd)
-{
-    const char *text = "Paused";
-    int id;
-
-    if ((id = gui_widget(pd, GUI_PAUSE)))
-    {
-        widget[id].text_img = make_image_from_font(NULL, NULL,
-                                                   &widget[id].w,
-                                                   &widget[id].h,
-                                                   text, font[GUI_LRG],
-                                                        scale[GUI_LRG]);
-        widget[id].color0 = gui_wht;
-        widget[id].color1 = gui_wht;
-        widget[id].value  = 0;
-        widget[id].size   = GUI_LRG;
-        widget[id].rect   = GUI_ALL;
-    }
-    return id;
-}
-
 /*---------------------------------------------------------------------------*/
 /*
  * Create  a multi-line  text box  using a  vertical array  of labels.
  * Parse the  text for '\'  characters and treat them  as line-breaks.
- * Preserve the rect specifation across the entire array.
+ * Preserve the rect specification across the entire array.
  */
 
 int gui_multi(int pd, const char *text, int size, int rect, const float *c0,
@@ -735,36 +881,29 @@ static void gui_vstack_up(int id)
     }
 }
 
-static void gui_paused_up(int id)
-{
-    /* Store width and height for later use in text rendering. */
-
-    widget[id].x = widget[id].w;
-    widget[id].y = widget[id].h;
-
-    /* The pause widget fills the screen. */
-
-    widget[id].w = config_get_d(CONFIG_WIDTH);
-    widget[id].h = config_get_d(CONFIG_HEIGHT);
-}
-
 static void gui_button_up(int id)
 {
     /* Store width and height for later use in text rendering. */
 
-    widget[id].x = widget[id].w;
-    widget[id].y = widget[id].h;
+    widget[id].text_obj_w = widget[id].w;
+    widget[id].text_obj_h = widget[id].h;
 
     if (widget[id].w < widget[id].h && widget[id].w > 0)
         widget[id].w = widget[id].h;
 
-
     /* Padded text elements look a little nicer. */
 
     if (widget[id].w < config_get_d(CONFIG_WIDTH))
         widget[id].w += radius;
     if (widget[id].h < config_get_d(CONFIG_HEIGHT))
         widget[id].h += radius;
+
+    /* A button should be at least wide enough to accomodate the rounding. */
+
+    if (widget[id].w < 2 * radius)
+        widget[id].w = 2 * radius;
+    if (widget[id].h < 2 * radius)
+        widget[id].h = 2 * radius;
 }
 
 static void gui_widget_up(int id)
@@ -776,7 +915,7 @@ static void gui_widget_up(int id)
         case GUI_VARRAY: gui_varray_up(id); break;
         case GUI_HSTACK: gui_hstack_up(id); break;
         case GUI_VSTACK: gui_vstack_up(id); break;
-        case GUI_PAUSE:  gui_paused_up(id); break;
+        case GUI_FILLER:                    break;
         default:         gui_button_up(id); break;
         }
 }
@@ -916,10 +1055,9 @@ static void gui_button_dn(int id, int x, int y, int w, int h)
 {
     /* Recall stored width and height for text rendering. */
 
-    int W = widget[id].x;
-    int H = widget[id].y;
+    int W = widget[id].text_obj_w;
+    int H = widget[id].text_obj_h;
     int R = widget[id].rect;
-    int r = ((widget[id].type & GUI_TYPE) == GUI_PAUSE ? radius * 4 : radius);
 
     const float *c0 = widget[id].color0;
     const float *c1 = widget[id].color1;
@@ -932,7 +1070,7 @@ static void gui_button_dn(int id, int x, int y, int w, int h)
     /* Create display lists for the text area and rounded rectangle. */
 
     widget[id].text_obj = gui_list(-W / 2, -H / 2, W, H, c0, c1);
-    widget[id].rect_obj = gui_rect(-w / 2, -h / 2, w, h, R, r);
+    widget[id].rect_obj = gui_rect(-w / 2, -h / 2, w, h, R, radius);
 }
 
 static void gui_widget_dn(int id, int x, int y, int w, int h)
@@ -1168,7 +1306,7 @@ static void gui_paint_count(int id)
                  widget[id].scale,
                  widget[id].scale);
 
-        if (widget[id].value)
+        if (widget[id].value > 0)
         {
             /* Translate left by half the total width of the rendered value. */
 
@@ -1186,7 +1324,7 @@ static void gui_paint_count(int id)
                 glTranslatef((GLfloat) -digit_w[i][j % 10], 0.0f, 0.0f);
             }
         }
-        else
+        else if (widget[id].value == 0)
         {
             /* If the value is zero, just display a zero in place. */
 
@@ -1210,6 +1348,9 @@ static void gui_paint_clock(int id)
     GLfloat dx_large = (GLfloat) digit_w[i][0];
     GLfloat dx_small = (GLfloat) digit_w[i][0] * 0.75f;
 
+    if (widget[id].value < 0)
+        return;
+
     glPushMatrix();
     {
         glColor4fv(gui_wht);
@@ -1313,37 +1454,28 @@ void gui_paint(int id)
 {
     if (id)
     {
-        glPushAttrib(GL_LIGHTING_BIT     |
-                     GL_COLOR_BUFFER_BIT |
-                     GL_DEPTH_BUFFER_BIT);
-        config_push_ortho();
+        video_push_ortho();
         {
-            glEnable(GL_BLEND);
             glEnable(GL_COLOR_MATERIAL);
             glDisable(GL_LIGHTING);
             glDisable(GL_DEPTH_TEST);
-
-            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
-
-            glPushAttrib(GL_TEXTURE_BIT);
             {
                 glDisable(GL_TEXTURE_2D);
                 gui_paint_rect(id, 0);
-            }
-            glPopAttrib();
 
-            gui_paint_text(id);
+                glEnable(GL_TEXTURE_2D);
+                gui_paint_text(id);
+
+                glColor4fv(gui_wht);
+            }
+            glEnable(GL_DEPTH_TEST);
+            glEnable(GL_LIGHTING);
+            glDisable(GL_COLOR_MATERIAL);
         }
-        config_pop_matrix();
-        glPopAttrib();
+        video_pop_matrix();
     }
 }
 
-void gui_blank(void)
-{
-    gui_paint(pause_id);
-}
-
 /*---------------------------------------------------------------------------*/
 
 void gui_dump(int id, int d)
@@ -1430,6 +1562,11 @@ int gui_point(int id, int x, int y)
         return active = jd;
 }
 
+void gui_focus(int i)
+{
+    active = i;
+}
+
 int gui_click(void)
 {
     return active;
@@ -1452,122 +1589,174 @@ void gui_toggle(int id)
 
 /*---------------------------------------------------------------------------*/
 
-static int gui_vert_test(int id, int jd)
+static int gui_vert_offset(int id, int jd)
 {
-    /* Determine whether widget id is in vertical contact with widget jd. */
-
-    if (id && gui_hot(id) && jd && gui_hot(jd))
-    {
-        int i0 = widget[id].x;
-        int i1 = widget[id].x + widget[id].w;
-        int j0 = widget[jd].x;
-        int j1 = widget[jd].x + widget[jd].w;
+    /* Vertical offset between bottom of id and top of jd */
 
-        /* Is widget id's top edge is in contact with jd's bottom edge? */
+    return  widget[id].y - (widget[jd].y + widget[jd].h);
+}
 
-        if (widget[id].y + widget[id].h == widget[jd].y)
-        {
-            /* Do widgets id and jd overlap horizontally? */
+static int gui_horz_offset(int id, int jd)
+{
+    /* Horizontal offset between left of id and right of jd */
 
-            if (j0 <= i0 && i0 <  j1) return 1;
-            if (j0 <  i1 && i1 <= j1) return 1;
-            if (i0 <= j0 && j0 <  i1) return 1;
-            if (i0 <  j1 && j1 <= i1) return 1;
-        }
-    }
-    return 0;
+    return  widget[id].x - (widget[jd].x + widget[jd].w);
 }
 
-static int gui_horz_test(int id, int jd)
+static int gui_vert_dist(int id, int jd)
 {
-    /* Determine whether widget id is in horizontal contact with widget jd. */
+    /* Vertical distance between the tops of id and jd */
 
-    if (id && gui_hot(id) && jd && gui_hot(jd))
-    {
-        int i0 = widget[id].y;
-        int i1 = widget[id].y + widget[id].h;
-        int j0 = widget[jd].y;
-        int j1 = widget[jd].y + widget[jd].h;
-
-        /* Is widget id's right edge in contact with jd's left edge? */
+    return abs((widget[id].y + widget[id].h) - (widget[jd].y + widget[jd].h));
+}
 
-        if (widget[id].x + widget[id].w == widget[jd].x)
-        {
-            /* Do widgets id and jd overlap vertically? */
+static int gui_horz_dist(int id, int jd)
+{
+    /* Horizontal distance between the left sides of id and jd */
 
-            if (j0 <= i0 && i0 <  j1) return 1;
-            if (j0 <  i1 && i1 <= j1) return 1;
-            if (i0 <= j0 && j0 <  i1) return 1;
-            if (i0 <  j1 && j1 <= i1) return 1;
-        }
-    }
-    return 0;
+    return abs(widget[id].x - widget[jd].x);
 }
 
 /*---------------------------------------------------------------------------*/
 
 static int gui_stick_L(int id, int dd)
 {
-    int jd, kd;
+    int jd, kd, hd;
+    int o, omin, d, dmin;
 
-    /* Find a widget to the left of widget dd. */
+    /* Find the closest "hot" widget to the left of dd (and inside id) */
 
-    if (gui_horz_test(id, dd))
+    if (id && gui_hot(id))
         return id;
 
+    hd = 0;
+    omin = widget[dd].x - widget[id].x + 1;
+    dmin = widget[dd].y + widget[dd].h + widget[id].y + widget[id].h;
+
     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
-        if ((kd = gui_stick_L(jd, dd)))
-            return kd;
+    {
+        kd = gui_stick_L(jd, dd);
 
-    return 0;
+        if (kd && kd != dd)
+        {
+            o = gui_horz_offset(dd, kd);
+            d = gui_vert_dist(dd, kd);
+
+            if (0 <= o && o <= omin && d <= dmin)
+            {
+                hd = kd;
+                omin = o;
+                dmin = d;
+            }
+        }
+    }
+
+    return hd;
 }
 
 static int gui_stick_R(int id, int dd)
 {
-    int jd, kd;
+    int jd, kd, hd;
+    int o, omin, d, dmin;
 
-    /* Find a widget to the right of widget dd. */
+    /* Find the closest "hot" widget to the right of dd (and inside id) */
 
-    if (gui_horz_test(dd, id))
+    if (id && gui_hot(id))
         return id;
 
+    hd = 0;
+    omin = (widget[id].x + widget[id].w) - (widget[dd].x + widget[dd].w) + 1;
+    dmin = widget[dd].y + widget[dd].h + widget[id].y + widget[id].h;
+
     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
-        if ((kd = gui_stick_R(jd, dd)))
-            return kd;
+    {
+        kd = gui_stick_R(jd, dd);
 
-    return 0;
+        if (kd && kd != dd)
+        {
+            o = gui_horz_offset(kd, dd);
+            d = gui_vert_dist(dd, kd);
+
+            if (0 <= o && o <= omin && d <= dmin)
+            {
+                hd = kd;
+                omin = o;
+                dmin = d;
+            }
+        }
+    }
+
+    return hd;
 }
 
 static int gui_stick_D(int id, int dd)
 {
-    int jd, kd;
+    int jd, kd, hd;
+    int o, omin, d, dmin;
 
-    /* Find a widget below widget dd. */
+    /* Find the closest "hot" widget below dd (and inside id) */
 
-    if (gui_vert_test(id, dd))
+    if (id && gui_hot(id))
         return id;
 
+    hd = 0;
+    omin = widget[dd].y - widget[id].y + 1;
+    dmin = widget[dd].x + widget[dd].w + widget[id].x + widget[id].w;
+
     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
-        if ((kd = gui_stick_D(jd, dd)))
-            return kd;
+    {
+        kd = gui_stick_D(jd, dd);
 
-    return 0;
+        if (kd && kd != dd)
+        {
+            o = gui_vert_offset(dd, kd);
+            d = gui_horz_dist(dd, kd);
+
+            if (0 <= o && o <= omin && d <= dmin)
+            {
+                hd = kd;
+                omin = o;
+                dmin = d;
+            }
+        }
+    }
+
+    return hd;
 }
 
 static int gui_stick_U(int id, int dd)
 {
-    int jd, kd;
+    int jd, kd, hd;
+    int o, omin, d, dmin;
 
-    /* Find a widget above widget dd. */
+    /* Find the closest "hot" widget above dd (and inside id) */
 
-    if (gui_vert_test(dd, id))
+    if (id && gui_hot(id))
         return id;
 
+    hd = 0;
+    omin = (widget[id].y + widget[id].h) - (widget[dd].y + widget[dd].h) + 1;
+    dmin = widget[dd].x + widget[dd].w + widget[id].x + widget[id].w;
+
     for (jd = widget[id].car; jd; jd = widget[jd].cdr)
-        if ((kd = gui_stick_U(jd, dd)))
-            return kd;
+    {
+        kd = gui_stick_U(jd, dd);
 
-    return 0;
+        if (kd && kd != dd)
+        {
+            o = gui_vert_offset(kd, dd);
+            d = gui_horz_dist(dd, kd);
+
+            if (0 <= o && o <= omin && d <= dmin)
+            {
+                hd = kd;
+                omin = o;
+                dmin = d;
+            }
+        }
+    }
+
+    return hd;
 }
 
 /*---------------------------------------------------------------------------*/
@@ -1618,13 +1807,21 @@ static int gui_wrap_D(int id, int dd)
 
 /*---------------------------------------------------------------------------*/
 
-int gui_stick(int id, int x, int y)
+/* Flag the axes to prevent uncontrolled scrolling. */
+
+static int xflag = 1;
+static int yflag = 1;
+
+void gui_stuck()
 {
-    /* Flag the axes to prevent uncontrolled scrolling. */
+    /* Force the user to recenter the joystick before the next GUI action. */
 
-    static int xflag = 1;
-    static int yflag = 1;
+    xflag = 0;
+    yflag = 0;
+}
 
+int gui_stick(int id, int x, int y)
+{
     int jd = 0;
 
     /* Find a new active widget in the direction of joystick motion. */