/* * Copyright (C) 2003 Robert Kooima * * NEVERBALL is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation; either version 2 of the License, * or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. */ #include #include #include #include #include "config.h" #include "video.h" #include "glext.h" #include "image.h" #include "vec3.h" #include "gui.h" #include "common.h" #include "font.h" #include "theme.h" #include "fs.h" #include "fs_rwops.h" /*---------------------------------------------------------------------------*/ /* Very pure colors for the GUI. I was watching BANZAI when I designed this. */ const GLubyte gui_wht[4] = { 0xFF, 0xFF, 0xFF, 0xFF }; /* White */ const GLubyte gui_yel[4] = { 0xFF, 0xFF, 0x00, 0xFF }; /* Yellow */ const GLubyte gui_red[4] = { 0xFF, 0x00, 0x00, 0xFF }; /* Red */ const GLubyte gui_grn[4] = { 0x00, 0xFF, 0x00, 0xFF }; /* Green */ const GLubyte gui_blu[4] = { 0x00, 0x00, 0xFF, 0xFF }; /* Blue */ const GLubyte gui_blk[4] = { 0x00, 0x00, 0x00, 0xFF }; /* Black */ const GLubyte gui_gry[4] = { 0x55, 0x55, 0x55, 0xFF }; /* Gray */ const GLubyte gui_shd[4] = { 0x00, 0x00, 0x00, 0x80 }; /* Shadow */ /*---------------------------------------------------------------------------*/ #define WIDGET_MAX 256 #define GUI_FREE 0 #define GUI_HARRAY 1 #define GUI_VARRAY 2 #define GUI_HSTACK 3 #define GUI_VSTACK 4 #define GUI_FILLER 5 #define GUI_IMAGE 6 #define GUI_LABEL 7 #define GUI_COUNT 8 #define GUI_CLOCK 9 #define GUI_SPACE 10 #define GUI_BUTTON 11 #define GUI_STATE 1 #define GUI_FILL 2 #define GUI_HILITE 4 #define GUI_RECT 8 #define GUI_LINES 8 /*---------------------------------------------------------------------------*/ struct widget { int type; int flags; int token; int value; int font; int size; int rect; const GLubyte *color0; const GLubyte *color1; int x, y; int w, h; int car; int cdr; GLuint image; GLfloat scale; int text_w; int text_h; enum trunc trunc; }; /*---------------------------------------------------------------------------*/ /* GUI widget state */ static struct widget widget[WIDGET_MAX]; static int active; static int hovered; static int clicked; static int padding; static int borders[4]; /* Digit widgets for the HUD. */ static int digit_id[3][11]; /* Cursor image. */ static int cursor_id = 0; static int cursor_st = 0; /* GUI theme. */ static struct theme curr_theme; /*---------------------------------------------------------------------------*/ static int gui_hot(int id) { return (widget[id].flags & GUI_STATE); } static int gui_size(void) { const int w = video.device_w; const int h = video.device_h; return MIN(w, h); } /*---------------------------------------------------------------------------*/ /* Vertex buffer definitions for widget rendering. */ /* Vertex count */ #define RECT_VERT 16 #define TEXT_VERT 8 #define IMAGE_VERT 4 #define WIDGET_VERT (RECT_VERT + MAX(TEXT_VERT, IMAGE_VERT)) /* Element count */ #define RECT_ELEM 28 #define WIDGET_ELEM (RECT_ELEM) struct vert { GLubyte c[4]; GLfloat u[2]; GLshort p[2]; }; static struct vert vert_buf[WIDGET_MAX * WIDGET_VERT]; static GLuint vert_vbo = 0; static GLuint vert_ebo = 0; /*---------------------------------------------------------------------------*/ static void set_vert(struct vert *v, int x, int y, GLfloat s, GLfloat t, const GLubyte *c) { v->c[0] = c[0]; v->c[1] = c[1]; v->c[2] = c[2]; v->c[3] = c[3]; v->u[0] = s; v->u[1] = t; v->p[0] = x; v->p[1] = y; } /*---------------------------------------------------------------------------*/ static void draw_enable(GLboolean c, GLboolean u, GLboolean p) { glBindBuffer_(GL_ARRAY_BUFFER, vert_vbo); glBindBuffer_(GL_ELEMENT_ARRAY_BUFFER, vert_ebo); if (c) { glEnableClientState(GL_COLOR_ARRAY); glColorPointer (4, GL_UNSIGNED_BYTE, sizeof (struct vert), (GLvoid *) offsetof (struct vert, c)); } if (u) { glEnableClientState(GL_TEXTURE_COORD_ARRAY); glTexCoordPointer(2, GL_FLOAT, sizeof (struct vert), (GLvoid *) offsetof (struct vert, u)); } if (p) { glEnableClientState(GL_VERTEX_ARRAY); glVertexPointer (2, GL_SHORT, sizeof (struct vert), (GLvoid *) offsetof (struct vert, p)); } } static void draw_rect(int id) { glDrawElements(GL_TRIANGLE_STRIP, RECT_ELEM, GL_UNSIGNED_SHORT, (const GLvoid *) (id * WIDGET_ELEM * sizeof (GLushort))); } static void draw_text(int id) { glDrawArrays(GL_TRIANGLE_STRIP, id * WIDGET_VERT + RECT_VERT, TEXT_VERT); } static void draw_image(int id) { glDrawArrays(GL_TRIANGLE_STRIP, id * WIDGET_VERT + RECT_VERT, IMAGE_VERT); } static void draw_disable(void) { glBindBuffer_(GL_ARRAY_BUFFER, 0); glBindBuffer_(GL_ELEMENT_ARRAY_BUFFER, 0); glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); glDisableClientState(GL_COLOR_ARRAY); } /*---------------------------------------------------------------------------*/ /* * Generate vertices for a 3x3 rectangle. Vertices are arranged * top-to-bottom and left-to-right, triangle strips are arranged * left-to-right and top-to-bottom (one strip per row). Degenerate * triangles (two extra indices per stitch) are inserted for a * continous strip. */ static const GLushort rect_elem_base[RECT_ELEM] = { 0, 1, 4, 5, 8, 9, 12, 13, 13, 1, /* Top */ 1, 2, 5, 6, 9, 10, 13, 14, 14, 2, /* Middle */ 2, 3, 6, 7, 10, 11, 14, 15 /* Bottom */ }; static void gui_geom_rect(int id, int x, int y, int w, int h, int f) { GLushort rect_elem[RECT_ELEM]; struct vert *v = vert_buf + id * WIDGET_VERT; struct vert *p = v; int X[4]; int Y[4]; int i, j; /* Generate vertex and element data for the widget's rectangle. */ X[0] = x; X[1] = x + ((f & GUI_W) ? borders[0] : 0); X[2] = x + w - ((f & GUI_E) ? borders[1] : 0); X[3] = x + w; Y[0] = y + h; Y[1] = y + h - ((f & GUI_N) ? borders[2] : 0); Y[2] = y + ((f & GUI_S) ? borders[3] : 0); Y[3] = y; for (i = 0; i < 4; i++) for (j = 0; j < 4; j++) set_vert(p++, X[i], Y[j], curr_theme.s[i], curr_theme.t[j], gui_wht); for (i = 0; i < RECT_ELEM; i++) rect_elem[i] = id * WIDGET_VERT + rect_elem_base[i]; /* Copy this off to the VBOs. */ glBindBuffer_ (GL_ARRAY_BUFFER, vert_vbo); glBufferSubData_(GL_ARRAY_BUFFER, id * WIDGET_VERT * sizeof (struct vert), RECT_VERT * sizeof (struct vert), v); glBindBuffer_ (GL_ARRAY_BUFFER, 0); glBindBuffer_ (GL_ELEMENT_ARRAY_BUFFER, vert_ebo); glBufferSubData_(GL_ELEMENT_ARRAY_BUFFER, id * WIDGET_ELEM * sizeof (GLushort), RECT_ELEM * sizeof (GLushort), rect_elem); glBindBuffer_ (GL_ELEMENT_ARRAY_BUFFER, 0); } static void gui_geom_text(int id, int x, int y, int w, int h, const GLubyte *c0, const GLubyte *c1) { struct vert *v = vert_buf + id * WIDGET_VERT + RECT_VERT; /* Assume the applied texture size is rect size rounded to power-of-two. */ int W; int H; image_size(&W, &H, w, h); if (w > 0 && h > 0 && W > 0 && H > 0) { const int d = h / 16; /* Shadow offset */ const int ww = ((W - w) % 2) ? w + 1 : w; const int hh = ((H - h) % 2) ? h + 1 : h; const GLfloat s0 = 0.5f * (W - ww) / W; const GLfloat t0 = 0.5f * (H - hh) / H; const GLfloat s1 = 1.0f - s0; const GLfloat t1 = 1.0f - t0; /* Generate vertex data for the colored text and its shadow. */ set_vert(v + 0, x + d, y + hh - d, s0, t0, gui_shd); set_vert(v + 1, x + d, y - d, s0, t1, gui_shd); set_vert(v + 2, x + ww + d, y + hh - d, s1, t0, gui_shd); set_vert(v + 3, x + ww + d, y - d, s1, t1, gui_shd); set_vert(v + 4, x, y + hh, s0, t0, c1); set_vert(v + 5, x, y, s0, t1, c0); set_vert(v + 6, x + ww, y + hh, s1, t0, c1); set_vert(v + 7, x + ww, y, s1, t1, c0); } else memset(v, 0, TEXT_VERT * sizeof (struct vert)); /* Copy this off to the VBO. */ glBindBuffer_ (GL_ARRAY_BUFFER, vert_vbo); glBufferSubData_(GL_ARRAY_BUFFER, (id * WIDGET_VERT + RECT_VERT) * sizeof (struct vert), TEXT_VERT * sizeof (struct vert), v); glBindBuffer_ (GL_ARRAY_BUFFER, 0); } static void gui_geom_image(int id, int x, int y, int w, int h, int f) { struct vert *v = vert_buf + id * WIDGET_VERT + RECT_VERT; int X[2]; int Y[2]; /* Trace inner vertices of the background rectangle. */ X[0] = x + ((f & GUI_W) ? borders[0] : 0); X[1] = x + w - ((f & GUI_E) ? borders[1] : 0); Y[0] = y + h - ((f & GUI_N) ? borders[2] : 0); Y[1] = y + ((f & GUI_S) ? borders[3] : 0); set_vert(v + 0, X[0], Y[0], 0.0f, 1.0f, gui_wht); set_vert(v + 1, X[0], Y[1], 0.0f, 0.0f, gui_wht); set_vert(v + 2, X[1], Y[0], 1.0f, 1.0f, gui_wht); set_vert(v + 3, X[1], Y[1], 1.0f, 0.0f, gui_wht); /* Copy this off to the VBO. */ glBindBuffer_ (GL_ARRAY_BUFFER, vert_vbo); glBufferSubData_(GL_ARRAY_BUFFER, (id * WIDGET_VERT + RECT_VERT) * sizeof (struct vert), IMAGE_VERT * sizeof (struct vert), v); glBindBuffer_ (GL_ARRAY_BUFFER, 0); } static void gui_geom_widget(int id, int flags) { int jd; int w = widget[id].w; int h = widget[id].h; int W = widget[id].text_w; int H = widget[id].text_h; int R = widget[id].rect; const GLubyte *c0 = widget[id].color0; const GLubyte *c1 = widget[id].color1; if ((widget[id].flags & GUI_RECT) && !(flags & GUI_RECT)) { gui_geom_rect(id, -w / 2, -h / 2, w, h, R); flags |= GUI_RECT; } switch (widget[id].type) { case GUI_FILLER: case GUI_SPACE: break; case GUI_HARRAY: case GUI_VARRAY: case GUI_HSTACK: case GUI_VSTACK: for (jd = widget[id].car; jd; jd = widget[jd].cdr) gui_geom_widget(jd, flags); break; case GUI_IMAGE: gui_geom_image(id, -w / 2, -h / 2, w, h, R); break; default: gui_geom_text(id, -W / 2, -H / 2, W, H, c0, c1); break; } } /*---------------------------------------------------------------------------*/ #define FONT_MAX 4 static struct font fonts[FONT_MAX]; static int fontc; static int font_sizes[3]; static int gui_font_load(const char *path) { int i; /* Find a previously loaded font. */ for (i = 0; i < fontc; i++) if (strcmp(fonts[i].path, path) == 0) return i; /* Load a new font. */ if (fontc < FONT_MAX) { if (font_load(&fonts[fontc], path, font_sizes)) { fontc++; return fontc - 1; } } /* Return index of default font. */ return 0; } static void gui_font_quit(void); static void gui_font_init(void) { gui_font_quit(); if (font_init()) { int s = gui_size(); font_sizes[0] = s / 26; font_sizes[1] = s / 13; font_sizes[2] = s / 7; /* Load the default font at index 0. */ gui_font_load(*curr_lang.font ? curr_lang.font : GUI_FACE); } } static void gui_font_quit(void) { int i; for (i = 0; i < fontc; i++) font_free(&fonts[i]); fontc = 0; font_quit(); } /*---------------------------------------------------------------------------*/ static void gui_theme_quit(void) { theme_free(&curr_theme); } static void gui_theme_init(void) { gui_theme_quit(); theme_load(&curr_theme, config_get_s(CONFIG_THEME)); } /*---------------------------------------------------------------------------*/ void gui_init(void) { const int s = gui_size(); int i, j; memset(widget, 0, sizeof (struct widget) * WIDGET_MAX); /* Compute default widget/text padding. */ padding = s / 60; for (i = 0; i < 4; i++) borders[i] = padding; /* Initialize font rendering. */ gui_font_init(); /* Initialize GUI theme. */ gui_theme_init(); /* Initialize the VBOs. */ memset(vert_buf, 0, sizeof (vert_buf)); glGenBuffers_(1, &vert_vbo); glBindBuffer_(GL_ARRAY_BUFFER, vert_vbo); glBufferData_(GL_ARRAY_BUFFER, sizeof (vert_buf), vert_buf, GL_STATIC_DRAW); glBindBuffer_(GL_ARRAY_BUFFER, 0); glGenBuffers_(1, &vert_ebo); glBindBuffer_(GL_ELEMENT_ARRAY_BUFFER, vert_ebo); glBufferData_(GL_ELEMENT_ARRAY_BUFFER, WIDGET_MAX * WIDGET_ELEM * sizeof (GLushort), NULL, GL_STATIC_DRAW); glBindBuffer_(GL_ELEMENT_ARRAY_BUFFER, 0); /* Cache digit glyphs for HUD rendering. */ for (i = 0; i < 3; i++) { digit_id[i][ 0] = gui_label(0, "0", i, 0, 0); digit_id[i][ 1] = gui_label(0, "1", i, 0, 0); digit_id[i][ 2] = gui_label(0, "2", i, 0, 0); digit_id[i][ 3] = gui_label(0, "3", i, 0, 0); digit_id[i][ 4] = gui_label(0, "4", i, 0, 0); digit_id[i][ 5] = gui_label(0, "5", i, 0, 0); digit_id[i][ 6] = gui_label(0, "6", i, 0, 0); digit_id[i][ 7] = gui_label(0, "7", i, 0, 0); digit_id[i][ 8] = gui_label(0, "8", i, 0, 0); digit_id[i][ 9] = gui_label(0, "9", i, 0, 0); digit_id[i][10] = gui_label(0, ":", i, 0, 0); } for (i = 0; i < 3; i++) for (j = 0; j < 11; ++j) gui_layout(digit_id[i][j], 0, 0); /* Cache an image for the cursor. Scale it to the same size as a digit. */ if ((cursor_id = gui_image(0, "gui/cursor.png", widget[digit_id[1][0]].w, widget[digit_id[1][0]].h))) gui_layout(cursor_id, 0, 0); active = 0; } void gui_free(void) { int id; /* Release the VBOs. */ glDeleteBuffers_(1, &vert_vbo); glDeleteBuffers_(1, &vert_ebo); /* Release any remaining widget texture and display list indices. */ for (id = 1; id < WIDGET_MAX; id++) { if (widget[id].image) glDeleteTextures(1, &widget[id].image); widget[id].type = GUI_FREE; widget[id].flags = 0; widget[id].image = 0; widget[id].cdr = 0; widget[id].car = 0; } /* Release all loaded fonts and finalize font rendering. */ gui_font_quit(); /* Release theme resources. */ gui_theme_quit(); } /*---------------------------------------------------------------------------*/ static int gui_widget(int pd, int type) { int id; /* Find an unused entry in the widget table. */ for (id = 1; id < WIDGET_MAX; id++) if (widget[id].type == GUI_FREE) { /* Set the type and default properties. */ widget[id].type = type; widget[id].flags = 0; widget[id].token = 0; widget[id].value = 0; widget[id].font = 0; widget[id].size = 0; widget[id].rect = GUI_ALL; widget[id].w = 0; widget[id].h = 0; widget[id].image = 0; widget[id].color0 = gui_wht; widget[id].color1 = gui_wht; widget[id].scale = 1.0f; widget[id].trunc = TRUNC_NONE; widget[id].text_w = 0; widget[id].text_h = 0; /* Insert the new widget into the parent's widget list. */ if (pd) { widget[id].car = 0; widget[id].cdr = widget[pd].car; widget[pd].car = id; } else { widget[id].car = 0; widget[id].cdr = 0; } return id; } log_printf("Out of widget IDs\n"); return 0; } int gui_harray(int pd) { return gui_widget(pd, GUI_HARRAY); } int gui_varray(int pd) { return gui_widget(pd, GUI_VARRAY); } int gui_hstack(int pd) { return gui_widget(pd, GUI_HSTACK); } int gui_vstack(int pd) { return gui_widget(pd, GUI_VSTACK); } int gui_filler(int pd) { return gui_widget(pd, GUI_FILLER); } /*---------------------------------------------------------------------------*/ static struct size gui_measure_ttf(const char *text, TTF_Font *font) { struct size size = { 0, 0 }; if (font) TTF_SizeUTF8(font, text, &size.w, &size.h); return size; } struct size gui_measure(const char *text, int size) { return gui_measure_ttf(text, fonts[0].ttf[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_ttf(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_ttf(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_ttf(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) { glDeleteTextures(1, &widget[id].image); widget[id].image = make_image_from_file(file, IF_MIPMAP); } void gui_set_label(int id, const char *text) { TTF_Font *ttf = fonts[widget[id].font].ttf[widget[id].size]; int w = 0; int h = 0; char *str; glDeleteTextures(1, &widget[id].image); str = gui_truncate(text, widget[id].w - padding, ttf, widget[id].trunc); widget[id].image = make_image_from_font(NULL, NULL, &widget[id].text_w, &widget[id].text_h, str, ttf, 0); w = widget[id].text_w; h = widget[id].text_h; gui_geom_text(id, -w / 2, -h / 2, w, h, widget[id].color0, widget[id].color1); free(str); } void gui_set_count(int id, int value) { widget[id].value = value; } void gui_set_clock(int id, int value) { widget[id].value = value; } void gui_set_color(int id, const GLubyte *c0, const GLubyte *c1) { if (id) { c0 = c0 ? c0 : gui_yel; c1 = c1 ? c1 : gui_red; if (widget[id].color0 != c0 || widget[id].color1 != c1) { int w = widget[id].text_w; int h = widget[id].text_h; widget[id].color0 = c0; widget[id].color1 = c1; gui_geom_text(id, -w / 2, -h / 2, w, h, c0, c1); } } } void gui_set_multi(int id, const char *text) { const char *p; char s[GUI_LINES][MAXSTR]; int i, sc, lc, jd; size_t n = 0; /* Count available labels. */ for (lc = 0, jd = widget[id].car; jd; lc++, jd = widget[jd].cdr); /* Copy each delimited string to a line buffer. */ for (p = text, sc = 0; *p && sc < lc; sc++) { strncpy(s[sc], p, (n = strcspn(p, "\\"))); s[sc][n] = 0; if (*(p += n) == '\\') p++; } /* Set the label value for each line. */ for (i = lc - 1, jd = widget[id].car; i >= 0; i--, jd = widget[jd].cdr) gui_set_label(jd, i < sc ? s[i] : ""); } void gui_set_trunc(int id, enum trunc trunc) { widget[id].trunc = trunc; } void gui_set_font(int id, const char *path) { widget[id].font = gui_font_load(path); } void gui_set_fill(int id) { widget[id].flags |= GUI_FILL; } /* * Activate a widget, allowing it to behave as a normal state widget. * This may be used to create image buttons, or cause an array of * widgets to behave as a single state widget. */ int gui_set_state(int id, int token, int value) { widget[id].flags |= GUI_STATE; widget[id].token = token; widget[id].value = value; return id; } void gui_set_hilite(int id, int hilite) { if (hilite) widget[id].flags |= GUI_HILITE; else widget[id].flags &= ~GUI_HILITE; } void gui_set_rect(int id, int rect) { widget[id].rect = rect; widget[id].flags |= GUI_RECT; } void gui_set_cursor(int st) { cursor_st = st; } /*---------------------------------------------------------------------------*/ int gui_image(int pd, const char *file, int w, int h) { int id; if ((id = gui_widget(pd, GUI_IMAGE))) { widget[id].image = make_image_from_file(file, IF_MIPMAP); widget[id].w = w; widget[id].h = h; widget[id].flags |= GUI_RECT; } return id; } int gui_start(int pd, const char *text, int size, int token, int value) { int id; if ((id = gui_state(pd, text, size, token, value))) active = id; return id; } int gui_state(int pd, const char *text, int size, int token, int value) { int id; if ((id = gui_widget(pd, GUI_BUTTON))) { TTF_Font *ttf = fonts[widget[id].font].ttf[size]; widget[id].flags |= (GUI_STATE | GUI_RECT); widget[id].image = make_image_from_font(NULL, NULL, &widget[id].text_w, &widget[id].text_h, text, ttf, 0); widget[id].w = widget[id].text_w; widget[id].h = widget[id].text_h; widget[id].size = size; widget[id].token = token; widget[id].value = value; } return id; } int gui_label(int pd, const char *text, int size, const GLubyte *c0, const GLubyte *c1) { int id; if ((id = gui_widget(pd, GUI_LABEL))) { TTF_Font *ttf = fonts[widget[id].font].ttf[size]; widget[id].image = make_image_from_font(NULL, NULL, &widget[id].text_w, &widget[id].text_h, text, ttf, 0); widget[id].w = widget[id].text_w; widget[id].h = widget[id].text_h; widget[id].size = size; widget[id].color0 = c0 ? c0 : gui_yel; widget[id].color1 = c1 ? c1 : gui_red; widget[id].flags |= GUI_RECT; } return id; } int gui_count(int pd, int value, int size) { int i, id; if ((id = gui_widget(pd, GUI_COUNT))) { for (i = value; i; i /= 10) widget[id].w += widget[digit_id[size][0]].text_w; widget[id].h = widget[digit_id[size][0]].text_h; widget[id].value = value; widget[id].size = size; widget[id].color0 = gui_yel; widget[id].color1 = gui_red; widget[id].flags |= GUI_RECT; } return id; } int gui_clock(int pd, int value, int size) { int id; if ((id = gui_widget(pd, GUI_CLOCK))) { widget[id].w = widget[digit_id[size][0]].text_w * 6; widget[id].h = widget[digit_id[size][0]].text_h; widget[id].value = value; widget[id].size = size; widget[id].color0 = gui_yel; widget[id].color1 = gui_red; widget[id].flags |= GUI_RECT; } return id; } int gui_space(int pd) { int id; if ((id = gui_widget(pd, GUI_SPACE))) { widget[id].w = 0; widget[id].h = 0; } 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 specification across the entire array. */ int gui_multi(int pd, const char *text, int size, const GLubyte *c0, const GLubyte *c1) { int id = 0; if (text && *text && (id = gui_varray(pd))) { const char *p; char s[GUI_LINES][MAXSTR]; int i, j; size_t n = 0; /* Copy each delimited string to a line buffer. */ for (p = text, j = 0; *p && j < GUI_LINES; j++) { strncpy(s[j], p, (n = strcspn(p, "\\"))); s[j][n] = 0; if (*(p += n) == '\\') p++; } /* Create a label widget for each line. */ for (i = 0; i < j; i++) gui_label(id, s[i], size, c0, c1); /* Set rectangle on the container. */ widget[id].flags |= GUI_RECT; } return id; } /*---------------------------------------------------------------------------*/ /* * The bottom-up pass determines the area of all widgets. The minimum * width and height of a leaf widget is given by the size of its * contents. Array and stack widths and heights are computed * recursively from these. */ static void gui_widget_up(int id); static void gui_harray_up(int id) { int jd, c = 0; /* Find the widest child width and the highest child height. */ for (jd = widget[id].car; jd; jd = widget[jd].cdr) { gui_widget_up(jd); if (widget[id].h < widget[jd].h) widget[id].h = widget[jd].h; if (widget[id].w < widget[jd].w) widget[id].w = widget[jd].w; c++; } /* Total width is the widest child width times the child count. */ widget[id].w *= c; } static void gui_varray_up(int id) { int jd, c = 0; /* Find the widest child width and the highest child height. */ for (jd = widget[id].car; jd; jd = widget[jd].cdr) { gui_widget_up(jd); if (widget[id].h < widget[jd].h) widget[id].h = widget[jd].h; if (widget[id].w < widget[jd].w) widget[id].w = widget[jd].w; c++; } /* Total height is the highest child height times the child count. */ widget[id].h *= c; } static void gui_hstack_up(int id) { int jd; /* Find the highest child height. Sum the child widths. */ for (jd = widget[id].car; jd; jd = widget[jd].cdr) { gui_widget_up(jd); if (widget[id].h < widget[jd].h) widget[id].h = widget[jd].h; widget[id].w += widget[jd].w; } } static void gui_vstack_up(int id) { int jd; /* Find the widest child width. Sum the child heights. */ for (jd = widget[id].car; jd; jd = widget[jd].cdr) { gui_widget_up(jd); if (widget[id].w < widget[jd].w) widget[id].w = widget[jd].w; widget[id].h += widget[jd].h; } } static void gui_button_up(int id) { 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 < video.device_w) widget[id].w += padding; if (widget[id].h < video.device_h) widget[id].h += padding; /* A button should be at least wide enough to accomodate the borders. */ if (widget[id].w < borders[0] + borders[1]) widget[id].w = borders[0] + borders[1]; if (widget[id].h < borders[2] + borders[3]) widget[id].h = borders[2] + borders[3]; } static void gui_widget_up(int id) { if (id) switch (widget[id].type) { case GUI_HARRAY: gui_harray_up(id); break; 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_FILLER: break; default: gui_button_up(id); break; } } /*---------------------------------------------------------------------------*/ /* * The top-down layout pass distributes available area as computed * during the bottom-up pass. Widgets use their area and position to * initialize rendering state. */ static void gui_widget_dn(int id, int x, int y, int w, int h); static void gui_harray_dn(int id, int x, int y, int w, int h) { int jd, i = 0, c = 0; widget[id].x = x; widget[id].y = y; widget[id].w = w; widget[id].h = h; /* Count children. */ for (jd = widget[id].car; jd; jd = widget[jd].cdr) c += 1; /* Distribute horizontal space evenly to all children. */ for (jd = widget[id].car; jd; jd = widget[jd].cdr, i++) { int x0 = x + i * w / c; int x1 = x + (i + 1) * w / c; gui_widget_dn(jd, x0, y, x1 - x0, h); } } static void gui_varray_dn(int id, int x, int y, int w, int h) { int jd, i = 0, c = 0; widget[id].x = x; widget[id].y = y; widget[id].w = w; widget[id].h = h; /* Count children. */ for (jd = widget[id].car; jd; jd = widget[jd].cdr) c += 1; /* Distribute vertical space evenly to all children. */ for (jd = widget[id].car; jd; jd = widget[jd].cdr, i++) { int y0 = y + i * h / c; int y1 = y + (i + 1) * h / c; gui_widget_dn(jd, x, y0, w, y1 - y0); } } static void gui_hstack_dn(int id, int x, int y, int w, int h) { int jd, jx = x, jw = 0, c = 0; widget[id].x = x; widget[id].y = y; widget[id].w = w; widget[id].h = h; /* Measure the total width requested by non-filler children. */ for (jd = widget[id].car; jd; jd = widget[jd].cdr) if (widget[jd].type == GUI_FILLER) c += 1; else if (widget[jd].flags & GUI_FILL) { c += 1; jw += widget[jd].w; } else jw += widget[jd].w; /* Give non-filler children their requested space. */ /* Distribute the rest evenly among filler children. */ for (jd = widget[id].car; jd; jd = widget[jd].cdr) { if (widget[jd].type == GUI_FILLER) gui_widget_dn(jd, jx, y, (w - jw) / c, h); else if (widget[jd].flags & GUI_FILL) gui_widget_dn(jd, jx, y, widget[jd].w + (w - jw) / c, h); else gui_widget_dn(jd, jx, y, widget[jd].w, h); jx += widget[jd].w; } } static void gui_vstack_dn(int id, int x, int y, int w, int h) { int jd, jy = y, jh = 0, c = 0; widget[id].x = x; widget[id].y = y; widget[id].w = w; widget[id].h = h; /* Measure the total height requested by non-filler children. */ for (jd = widget[id].car; jd; jd = widget[jd].cdr) if (widget[jd].type == GUI_FILLER) c += 1; else if (widget[jd].flags & GUI_FILL) { c += 1; jh += widget[jd].h; } else jh += widget[jd].h; /* Give non-filler children their requested space. */ /* Distribute the rest evenly among filler children. */ for (jd = widget[id].car; jd; jd = widget[jd].cdr) { if (widget[jd].type == GUI_FILLER) gui_widget_dn(jd, x, jy, w, (h - jh) / c); else if (widget[jd].flags & GUI_FILL) gui_widget_dn(jd, x, jy, w, widget[jd].h + (h - jh) / c); else gui_widget_dn(jd, x, jy, w, widget[jd].h); jy += widget[jd].h; } } static void gui_filler_dn(int id, int x, int y, int w, int h) { /* Filler expands to whatever size it is given. */ widget[id].x = x; widget[id].y = y; widget[id].w = w; widget[id].h = h; } static void gui_button_dn(int id, int x, int y, int w, int h) { widget[id].x = x; widget[id].y = y; widget[id].w = w; widget[id].h = h; } static void gui_widget_dn(int id, int x, int y, int w, int h) { if (id) switch (widget[id].type) { case GUI_HARRAY: gui_harray_dn(id, x, y, w, h); break; case GUI_VARRAY: gui_varray_dn(id, x, y, w, h); break; case GUI_HSTACK: gui_hstack_dn(id, x, y, w, h); break; case GUI_VSTACK: gui_vstack_dn(id, x, y, w, h); break; case GUI_FILLER: gui_filler_dn(id, x, y, w, h); break; case GUI_SPACE: gui_filler_dn(id, x, y, w, h); break; default: gui_button_dn(id, x, y, w, h); break; } } /*---------------------------------------------------------------------------*/ /* * During GUI layout, we make a bottom-up pass to determine total area * requirements for the widget tree. We position this area to the * sides or center of the screen. Finally, we make a top-down pass to * distribute this area to each widget. */ void gui_layout(int id, int xd, int yd) { int x, y; int w, W = video.device_w; int h, H = video.device_h; gui_widget_up(id); w = widget[id].w; h = widget[id].h; if (xd < 0) x = 0; else if (xd > 0) x = (W - w); else x = (W - w) / 2; if (yd < 0) y = 0; else if (yd > 0) y = (H - h); else y = (H - h) / 2; gui_widget_dn(id, x, y, w, h); /* Set up GUI rendering state. */ gui_geom_widget(id, 0); /* Hilite the widget under the cursor, if any. */ gui_point(id, -1, -1); } int gui_search(int id, int x, int y) { int jd, kd; /* Search the hierarchy for the widget containing the given point. */ if (id && (widget[id].x <= x && x < widget[id].x + widget[id].w && widget[id].y <= y && y < widget[id].y + widget[id].h)) { if (gui_hot(id)) return id; for (jd = widget[id].car; jd; jd = widget[jd].cdr) if ((kd = gui_search(jd, x, y))) return kd; } return 0; } int gui_delete(int id) { if (id) { /* Recursively delete all subwidgets. */ gui_delete(widget[id].cdr); gui_delete(widget[id].car); /* Release any GL resources held by this widget. */ if (widget[id].image) glDeleteTextures(1, &widget[id].image); /* Mark this widget unused. */ widget[id].type = GUI_FREE; widget[id].flags = 0; widget[id].image = 0; widget[id].cdr = 0; widget[id].car = 0; /* Clear focus from this widget. */ if (active == id) active = 0; } return 0; } /*---------------------------------------------------------------------------*/ static void gui_paint_rect(int id, int st, int flags) { int jd, i = 0; /* Use the widget status to determine the background color. */ i = st | (((widget[id].flags & GUI_HILITE) ? 2 : 0) | ((id == active) ? 1 : 0)); if ((widget[id].flags & GUI_RECT) && !(flags & GUI_RECT)) { /* Draw a leaf's background, colored by widget state. */ glPushMatrix(); { glTranslatef((GLfloat) (widget[id].x + widget[id].w / 2), (GLfloat) (widget[id].y + widget[id].h / 2), 0.f); glBindTexture(GL_TEXTURE_2D, curr_theme.tex[i]); draw_rect(id); } glPopMatrix(); flags |= GUI_RECT; } switch (widget[id].type) { case GUI_HARRAY: case GUI_VARRAY: case GUI_HSTACK: case GUI_VSTACK: /* Recursively paint all subwidgets. */ for (jd = widget[id].car; jd; jd = widget[jd].cdr) gui_paint_rect(jd, i, flags); break; } } /*---------------------------------------------------------------------------*/ static void gui_paint_text(int id); static void gui_paint_array(int id) { int jd; glPushMatrix(); { GLfloat cx = widget[id].x + widget[id].w / 2.0f; GLfloat cy = widget[id].y + widget[id].h / 2.0f; GLfloat ck = widget[id].scale; if (1.0f < ck || ck < 1.0f) { glTranslatef(+cx, +cy, 0.0f); glScalef(ck, ck, ck); glTranslatef(-cx, -cy, 0.0f); } /* Recursively paint all subwidgets. */ for (jd = widget[id].car; jd; jd = widget[jd].cdr) gui_paint_text(jd); } glPopMatrix(); } static void gui_paint_image(int id) { /* Draw the widget rect, textured using the image. */ glPushMatrix(); { glTranslatef((GLfloat) (widget[id].x + widget[id].w / 2), (GLfloat) (widget[id].y + widget[id].h / 2), 0.f); glScalef(widget[id].scale, widget[id].scale, widget[id].scale); glBindTexture(GL_TEXTURE_2D, widget[id].image); glColor4ub(gui_wht[0], gui_wht[1], gui_wht[2], gui_wht[3]); draw_image(id); } glPopMatrix(); } static void gui_paint_count(int id) { int j, i = widget[id].size; glPushMatrix(); { /* Translate to the widget center, and apply the pulse scale. */ glTranslatef((GLfloat) (widget[id].x + widget[id].w / 2), (GLfloat) (widget[id].y + widget[id].h / 2), 0.f); glScalef(widget[id].scale, widget[id].scale, widget[id].scale); if (widget[id].value > 0) { /* Translate right by half the total width of the rendered value. */ GLfloat w = -widget[digit_id[i][0]].text_w * 0.5f; for (j = widget[id].value; j; j /= 10) w += widget[digit_id[i][j % 10]].text_w * 0.5f; glTranslatef(w, 0.0f, 0.0f); /* Render each digit, moving left after each. */ for (j = widget[id].value; j; j /= 10) { int jd = digit_id[i][j % 10]; glBindTexture(GL_TEXTURE_2D, widget[jd].image); draw_text(jd); glTranslatef((GLfloat) -widget[jd].text_w, 0.0f, 0.0f); } } else if (widget[id].value == 0) { /* If the value is zero, just display a zero in place. */ glBindTexture(GL_TEXTURE_2D, widget[digit_id[i][0]].image); draw_text(digit_id[i][0]); } } glPopMatrix(); } static void gui_paint_clock(int id) { int i = widget[id].size; int mt = (widget[id].value / 6000) / 10; int mo = (widget[id].value / 6000) % 10; int st = ((widget[id].value % 6000) / 100) / 10; int so = ((widget[id].value % 6000) / 100) % 10; int ht = ((widget[id].value % 6000) % 100) / 10; int ho = ((widget[id].value % 6000) % 100) % 10; GLfloat dx_large = (GLfloat) widget[digit_id[i][0]].text_w; GLfloat dx_small = (GLfloat) widget[digit_id[i][0]].text_w * 0.75f; if (widget[id].value < 0) return; glPushMatrix(); { /* Translate to the widget center, and apply the pulse scale. */ glTranslatef((GLfloat) (widget[id].x + widget[id].w / 2), (GLfloat) (widget[id].y + widget[id].h / 2), 0.f); glScalef(widget[id].scale, widget[id].scale, widget[id].scale); /* Translate left by half the total width of the rendered value. */ if (mt > 0) glTranslatef(-2.25f * dx_large, 0.0f, 0.0f); else glTranslatef(-1.75f * dx_large, 0.0f, 0.0f); /* Render the minutes counter. */ if (mt > 0) { glBindTexture(GL_TEXTURE_2D, widget[digit_id[i][mt]].image); draw_text(digit_id[i][mt]); glTranslatef(dx_large, 0.0f, 0.0f); } glBindTexture(GL_TEXTURE_2D, widget[digit_id[i][mo]].image); draw_text(digit_id[i][mo]); glTranslatef(dx_small, 0.0f, 0.0f); /* Render the colon. */ glBindTexture(GL_TEXTURE_2D, widget[digit_id[i][10]].image); draw_text(digit_id[i][10]); glTranslatef(dx_small, 0.0f, 0.0f); /* Render the seconds counter. */ glBindTexture(GL_TEXTURE_2D, widget[digit_id[i][st]].image); draw_text(digit_id[i][st]); glTranslatef(dx_large, 0.0f, 0.0f); glBindTexture(GL_TEXTURE_2D, widget[digit_id[i][so]].image); draw_text(digit_id[i][so]); glTranslatef(dx_small, 0.0f, 0.0f); /* Render hundredths counter half size. */ glScalef(0.5f, 0.5f, 1.0f); glBindTexture(GL_TEXTURE_2D, widget[digit_id[i][ht]].image); draw_text(digit_id[i][ht]); glTranslatef(dx_large, 0.0f, 0.0f); glBindTexture(GL_TEXTURE_2D, widget[digit_id[i][ho]].image); draw_text(digit_id[i][ho]); } glPopMatrix(); } static void gui_paint_label(int id) { /* Short-circuit empty labels. */ if (widget[id].image == 0) return; /* Draw the widget text box, textured using the glyph. */ glPushMatrix(); { glTranslatef((GLfloat) (widget[id].x + widget[id].w / 2), (GLfloat) (widget[id].y + widget[id].h / 2), 0.f); glScalef(widget[id].scale, widget[id].scale, widget[id].scale); glBindTexture(GL_TEXTURE_2D, widget[id].image); draw_text(id); } glPopMatrix(); } static void gui_paint_text(int id) { switch (widget[id].type) { case GUI_SPACE: break; case GUI_FILLER: break; case GUI_HARRAY: gui_paint_array(id); break; case GUI_VARRAY: gui_paint_array(id); break; case GUI_HSTACK: gui_paint_array(id); break; case GUI_VSTACK: gui_paint_array(id); break; case GUI_IMAGE: gui_paint_image(id); break; case GUI_COUNT: gui_paint_count(id); break; case GUI_CLOCK: gui_paint_clock(id); break; default: gui_paint_label(id); break; } } void gui_paint(int id) { if (id) { video_push_ortho(); { glDisable(GL_LIGHTING); glDisable(GL_DEPTH_TEST); { draw_enable(GL_FALSE, GL_TRUE, GL_TRUE); gui_paint_rect(id, 0, 0); draw_enable(GL_TRUE, GL_TRUE, GL_TRUE); gui_paint_text(id); if (cursor_st && cursor_id) gui_paint_image(cursor_id); draw_disable(); glColor4ub(gui_wht[0], gui_wht[1], gui_wht[2], gui_wht[3]); } glEnable(GL_DEPTH_TEST); glEnable(GL_LIGHTING); } video_pop_matrix(); } } /*---------------------------------------------------------------------------*/ void gui_dump(int id, int d) { int jd, i; if (id) { char *type = "?"; switch (widget[id].type) { case GUI_HARRAY: type = "harray"; break; case GUI_VARRAY: type = "varray"; break; case GUI_HSTACK: type = "hstack"; break; case GUI_VSTACK: type = "vstack"; break; case GUI_FILLER: type = "filler"; break; case GUI_IMAGE: type = "image"; break; case GUI_LABEL: type = "label"; break; case GUI_COUNT: type = "count"; break; case GUI_CLOCK: type = "clock"; break; case GUI_BUTTON: type = "button"; break; } for (i = 0; i < d; i++) printf(" "); printf("%04d %s\n", id, type); for (jd = widget[id].car; jd; jd = widget[jd].cdr) gui_dump(jd, d + 1); } } void gui_pulse(int id, float k) { if (id) widget[id].scale = k; } void gui_timer(int id, float dt) { int jd; if (id) { for (jd = widget[id].car; jd; jd = widget[jd].cdr) gui_timer(jd, dt); if (widget[id].scale - 1.0f < dt) widget[id].scale = 1.0f; else widget[id].scale -= dt; } } int gui_point(int id, int x, int y) { static int x_cache = 0; static int y_cache = 0; int jd; /* Reuse the last coordinates if (x,y) == (-1,-1) */ if (x < 0 && y < 0) return gui_point(id, x_cache, y_cache); x_cache = x; y_cache = y; /* Move the cursor, if any. */ if (cursor_id) { widget[cursor_id].x = x - widget[cursor_id].w / 2; widget[cursor_id].y = y - widget[cursor_id].h / 2; } /* Short-circuit check the current active widget. */ jd = gui_search(active, x, y); /* If not still active, search the hierarchy for a new active widget. */ if (jd == 0) jd = gui_search(id, x, y); /* Note hovered widget. */ hovered = jd; /* If the active widget has changed, return the new active id. */ if (jd == 0 || jd == active) return 0; else return active = jd; } void gui_focus(int i) { active = i; } int gui_active(void) { return active; } int gui_token(int id) { return id ? widget[id].token : 0; } int gui_value(int id) { return id ? widget[id].value : 0; } void gui_toggle(int id) { widget[id].flags ^= GUI_HILITE; } /*---------------------------------------------------------------------------*/ static int gui_vert_offset(int id, int jd) { /* Vertical offset between bottom of id and top of jd */ return widget[id].y - (widget[jd].y + widget[jd].h); } static int gui_horz_offset(int id, int jd) { /* Horizontal offset between left of id and right of jd */ return widget[id].x - (widget[jd].x + widget[jd].w); } static int gui_vert_overlap(int id, int jd) { /* Extent of vertical intersection of id and jd. */ const int a0 = widget[id].y; const int a1 = widget[id].y + widget[id].h; const int aw = widget[id].h; const int b0 = widget[jd].y; const int b1 = widget[jd].y + widget[jd].h; /* const int bw = widget[jd].h; */ return aw + MIN(b1 - a1, 0) - MAX(b0 - a0, 0); } static int gui_horz_overlap(int id, int jd) { /* Extent of horizontal intersection of id and jd. */ const int a0 = widget[id].x; const int a1 = widget[id].x + widget[id].w; const int aw = widget[id].w; const int b0 = widget[jd].x; const int b1 = widget[jd].x + widget[jd].w; /* const int bw = widget[jd].w; */ return aw + MIN(b1 - a1, 0) - MAX(b0 - a0, 0); } /*---------------------------------------------------------------------------*/ /* * Widget navigation heuristics. * * People generally read left-to-right and top-to-bottom, and have * expectations on how navigation should behave. Thus, we hand-craft a * bunch of rules rather than devise a generic algorithm. * * Horizontal navigation only picks overlapping widgets. Closer is * better. Out of multiple closest widgets pick the topmost widget. * * Vertical navigation picks both overlapping and non-overlapping * widgets. Closer is better. Out of multiple closest widgets: if * overlapping, pick the leftmost widget; if not overlapping, pick the * one with the least negative overlap. */ /* * Leftmost/topmost is decided by the operator used to test * distance. A less-than will pick the first of a group of * equal-distance widgets, while a less-or-equal will pick the last * one. */ /* FIXME This isn't how you factor out reusable code. */ #define CHECK_HORIZONTAL \ (o > 0 && (omin > 0 ? d <= dmin : 1)) #define CHECK_VERTICAL \ (omin > 0 ? \ d < dmin : \ (o > 0 ? d <= dmin : (d < dmin || (d == dmin && o > omin)))) static int gui_stick_L(int id, int dd) { int jd, kd, hd; int d, dmin, o, omin; /* Find the closest "hot" widget to the left of dd (and inside id) */ if (id && gui_hot(id)) return id; hd = 0; dmin = widget[dd].x - widget[id].x + 1; omin = INT_MIN; for (jd = widget[id].car; jd; jd = widget[jd].cdr) { kd = gui_stick_L(jd, dd); if (kd && kd != dd) { d = gui_horz_offset(dd, kd); o = gui_vert_overlap(dd, kd); if (d >= 0 && CHECK_HORIZONTAL) { hd = kd; dmin = d; omin = o; } } } return hd; } static int gui_stick_R(int id, int dd) { int jd, kd, hd; int d, dmin, o, omin; /* Find the closest "hot" widget to the right of dd (and inside id) */ if (id && gui_hot(id)) return id; hd = 0; dmin = (widget[id].x + widget[id].w) - (widget[dd].x + widget[dd].w) + 1; omin = INT_MIN; for (jd = widget[id].car; jd; jd = widget[jd].cdr) { kd = gui_stick_R(jd, dd); if (kd && kd != dd) { d = gui_horz_offset(kd, dd); o = gui_vert_overlap(dd, kd); if (d >= 0 && CHECK_HORIZONTAL) { hd = kd; dmin = d; omin = o; } } } return hd; } static int gui_stick_D(int id, int dd) { int jd, kd, hd; int d, dmin, o, omin; /* Find the closest "hot" widget below dd (and inside id) */ if (id && gui_hot(id)) return id; hd = 0; dmin = widget[dd].y - widget[id].y + 1; omin = INT_MIN; for (jd = widget[id].car; jd; jd = widget[jd].cdr) { kd = gui_stick_D(jd, dd); if (kd && kd != dd) { d = gui_vert_offset(dd, kd); o = gui_horz_overlap(dd, kd); if (d >= 0 && CHECK_VERTICAL) { hd = kd; dmin = d; omin = o; } } } return hd; } static int gui_stick_U(int id, int dd) { int jd, kd, hd; int d, dmin, o, omin; /* Find the closest "hot" widget above dd (and inside id) */ if (id && gui_hot(id)) return id; hd = 0; dmin = (widget[id].y + widget[id].h) - (widget[dd].y + widget[dd].h) + 1; omin = INT_MIN; for (jd = widget[id].car; jd; jd = widget[jd].cdr) { kd = gui_stick_U(jd, dd); if (kd && kd != dd) { d = gui_vert_offset(kd, dd); o = gui_horz_overlap(dd, kd); if (d >= 0 && CHECK_VERTICAL) { hd = kd; dmin = d; omin = o; } } } return hd; } /*---------------------------------------------------------------------------*/ static int gui_wrap_L(int id, int dd) { int jd, kd; if ((jd = gui_stick_L(id, dd)) == 0) for (jd = dd; (kd = gui_stick_R(id, jd)); jd = kd) ; return jd; } static int gui_wrap_R(int id, int dd) { int jd, kd; if ((jd = gui_stick_R(id, dd)) == 0) for (jd = dd; (kd = gui_stick_L(id, jd)); jd = kd) ; return jd; } static int gui_wrap_U(int id, int dd) { int jd, kd; if ((jd = gui_stick_U(id, dd)) == 0) for (jd = dd; (kd = gui_stick_D(id, jd)); jd = kd) ; return jd; } static int gui_wrap_D(int id, int dd) { int jd, kd; if ((jd = gui_stick_D(id, dd)) == 0) for (jd = dd; (kd = gui_stick_U(id, jd)); jd = kd) ; return jd; } /*---------------------------------------------------------------------------*/ int gui_stick(int id, int a, float v, int bump) { int jd = 0; if (!bump) return 0; /* Find a new active widget in the direction of joystick motion. */ if (config_tst_d(CONFIG_JOYSTICK_AXIS_X0, a)) { if (v < 0) jd = gui_wrap_L(id, active); if (v > 0) jd = gui_wrap_R(id, active); } else if (config_tst_d(CONFIG_JOYSTICK_AXIS_Y0, a)) { if (v < 0) jd = gui_wrap_U(id, active); if (v > 0) jd = gui_wrap_D(id, active); } /* If the active widget has changed, return the new active id. */ if (jd == 0 || jd == active) return 0; else return active = jd; } int gui_click(int b, int d) { if (b == SDL_BUTTON_LEFT) { if (d) { clicked = hovered; return 0; } else { int c = (clicked && clicked == hovered); clicked = 0; return c; } } return 0; } /*---------------------------------------------------------------------------*/ int gui_navig(int id, int total, int first, int step) { int pages = (int) ceil((double) total / step); int page = first / step + 1; int prev = (page > 1); int next = (page < pages); int jd, kd; if ((jd = gui_hstack(id))) { if (next || prev) { gui_maybe(jd, " > ", GUI_NEXT, GUI_NONE, next); if ((kd = gui_label(jd, "999/999", GUI_SML, gui_wht, gui_wht))) { char str[16]; sprintf(str, "%d/%d", page, pages); gui_set_label(kd, str); } gui_maybe(jd, " < ", GUI_PREV, GUI_NONE, prev); } gui_space(jd); gui_start(jd, _("Back"), GUI_SML, GUI_BACK, 0); } return jd; } int gui_maybe(int id, const char *label, int etoken, int dtoken, int enabled) { int bd; if (!enabled) { bd = gui_state(id, label, GUI_SML, dtoken, 0); gui_set_color(bd, gui_gry, gui_gry); } else bd = gui_state(id, label, GUI_SML, etoken, 0); return bd; } /*---------------------------------------------------------------------------*/