/********************************************************************* * * Copyright (C) 2004,2008, Simon Kagstrom * * Filename: menu.c * Author: Simon Kagstrom * Description: Code for menus (originally for Mophun) * * $Id$ * ********************************************************************/ #include #include #include "menu.hh" #include "utils.hh" #include "menutexts.h" #define IS_SUBMENU(p_msg) ( (p_msg)[0] == '^' ) #define IS_TEXT(p_msg) ( (p_msg)[0] == '#' || (p_msg)[0] == ' ' ) #define IS_MARKER(p_msg) ( (p_msg)[0] == '@' ) void menu_print_font(SDL_Surface *screen, int r, int g, int b, int x, int y, const char *msg) { #define _MAX_STRING 64 SDL_Surface *font_surf; SDL_Rect dst = {x, y, 0, 0}; SDL_Color color = {r, g, b}; char buf[255]; unsigned int i; memset(buf, 0, sizeof(buf)); strncpy(buf, msg, 254); if (buf[0] != '|' && buf[0] != '^' && buf[0] != '.' && buf[0] != '-' && buf[0] != ' ' && !strstr(buf, " \"")) { if (strlen(buf)>_MAX_STRING) { buf[_MAX_STRING-3] = '.'; buf[_MAX_STRING-2] = '.'; buf[_MAX_STRING-1] = '.'; buf[_MAX_STRING] = '\0'; } } /* Fixup multi-menu option look */ for (i = 0; i < strlen(buf) ; i++) { if (buf[i] == '^' || buf[i] == '|') buf[i] = ' '; } font_surf = TTF_RenderText_Blended(menu_font, buf, color); if (!font_surf) { fprintf(stderr, "%s\n", TTF_GetError()); exit(1); } SDL_BlitSurface(font_surf, NULL, screen, &dst); SDL_FreeSurface(font_surf); } static void menu_draw(SDL_Surface *screen, menu_t *p_menu, int sel) { int font_height = TTF_FontHeight(p_menu->p_font); int line_height = (font_height + font_height / 4); int x_start = p_menu->x1; int y_start = p_menu->y1 + line_height; SDL_Rect r; int entries_visible = (p_menu->y2 - p_menu->y1) / line_height - 2; int i, y; char pTemp[256]; if ( p_menu->n_entries * line_height > p_menu->y2 ) y_start = p_menu->y1 + line_height; if (p_menu->cur_sel - p_menu->start_entry_visible > entries_visible) { while (p_menu->cur_sel - p_menu->start_entry_visible > entries_visible) { p_menu->start_entry_visible ++; if (p_menu->start_entry_visible > p_menu->n_entries) { p_menu->start_entry_visible = 0; break; } } } else if ( p_menu->cur_sel < p_menu->start_entry_visible ) p_menu->start_entry_visible = p_menu->cur_sel; if (strlen(p_menu->title)) { r.x = p_menu->x1; r.y = p_menu->y1; r.w = p_menu->x2 - p_menu->x1; r.h = line_height-1; if (sel < 0) SDL_FillRect(screen, &r, SDL_MapRGB(screen->format, 0x40, 0x00, 0x00)); else SDL_FillRect(screen, &r, SDL_MapRGB(screen->format, 0x00, 0x00, 0xff)); menu_print_font(screen, 0,0,0, p_menu->x1, p_menu->y1, p_menu->title); } for (i = p_menu->start_entry_visible; i <= p_menu->start_entry_visible + entries_visible; i++) { const char *msg = p_menu->pp_msgs[i]; if (i >= p_menu->n_entries) break; if (IS_MARKER(msg)) p_menu->cur_sel = atoi(&msg[1]); else { y = (i - p_menu->start_entry_visible) * line_height; if (sel < 0) menu_print_font(screen, 0x40,0x40,0x40, x_start, y_start + y, msg); else if (p_menu->cur_sel == i) /* Selected - color */ menu_print_font(screen, 0,255,0, x_start, y_start + y, msg); else if (IS_SUBMENU(msg)) { if (p_menu->cur_sel == i-1) menu_print_font(screen, 0x80,0xff,0x80, x_start, y_start + y, msg); else menu_print_font(screen, 0x40,0x40,0x40, x_start, y_start + y, msg); } else if (msg[0] == '#') { switch (msg[1]) { case '1': menu_print_font(screen, 0,0,255, x_start, y_start + y, msg+2); break; case '2': menu_print_font(screen, 0x80,0x80,0x80, x_start, y_start + y, msg+2); break; default: menu_print_font(screen, 0x40,0x40,0x40, x_start, y_start + y, msg); break; } } else /* Otherwise white */ menu_print_font(screen, 0x40,0x40,0x40, x_start, y_start + y, msg); if (IS_SUBMENU(msg)) { submenu_t *p_submenu = find_submenu(p_menu, i); int n_pipe = 0; int n; for (n=0; msg[n] != '\0'; n++) { /* Underline the selected entry */ if (msg[n] == '|') { int16_t n_chars; for (n_chars = 1; msg[n+n_chars] && msg[n+n_chars] != '|'; n_chars++); n_pipe++; if (p_submenu->sel == n_pipe-1) { int w; int h; if (TTF_SizeText(p_menu->p_font, "X", &w, &h) < 0) { fw = w; fh = h; fprintf(stderr, "%s\n", TTF_GetError()); exit(1); } r = (SDL_Rect){ x_start + (n+1) * w-1, y_start + (i+ 1 - p_menu->start_entry_visible) * ((h + h/4)) -3, (n_chars - 1) * w, 2}; if (p_menu->cur_sel == i-1) SDL_FillRect(screen, &r, SDL_MapRGB(screen->format, 0x0,0xff,0x80)); else SDL_FillRect(screen, &r, SDL_MapRGB(screen->format, 0x40,0x40,0x40)); break; } } } } } } } void Menu::printText(const char *msg, SDL_Color clr, int x, int y, int w, int h) { SDL_Surface *font_surf; SDL_Rect dst = {x, y, 0, 0}; SDL_Color color = {r, g, b}; char buf[255]; unsigned int i; int tw, th; TTF_SizeText(this->font, msg, &tw, &th); memset(buf, 0, sizeof(buf)); strncpy(buf, msg, 254); /* Crop text */ if (x + tw > w) { int pixels_per_char = tw / strlen(msg); int last_char = w / pixels_per_char; /* FIXME! Handle some corner cases here (short strings etc) */ } if (buf[0] != '|' && buf[0] != '^' && buf[0] != '.' && buf[0] != '-' && buf[0] != ' ' && !strstr(buf, " \"")) { if (strlen(buf)>_MAX_STRING) { buf[_MAX_STRING-3] = '.'; buf[_MAX_STRING-2] = '.'; buf[_MAX_STRING-1] = '.'; buf[_MAX_STRING] = '\0'; } } /* Fixup multi-menu option look */ for (i = 0; i < strlen(buf) ; i++) { if (buf[i] == '^' || buf[i] == '|') buf[i] = ' '; } font_surf = TTF_RenderText_Blended(menu_font, buf, color); if (!font_surf) { fprintf(stderr, "%s\n", TTF_GetError()); exit(1); } SDL_BlitSurface(font_surf, NULL, screen, &dst); SDL_FreeSurface(font_surf); } void Menu::highlightBackground(int x, int y, int w, int h) { /* Can't highlight without images */ if (!this->text_bg_left || !this->text_bg_middle || !this->text_bg_right) return; int bg_y_start = y + font_height / 2 - this->text_bg_left->h / 2; int bg_x_start = x - this->text_bg_right / 2; int bg_x_end = x + w - this->text_bg_right / 2; int n_mid = this->text_bg_left->w + this->text_bg_right->w - this->text_bg_middle->w; SDL_BlitSurface(this->text_bg_left, NULL, where, (SDL_Rect){bg_x_start, bg_y_start, 0,0}); for (int i = 1; i < n_mid; i++) SDL_BlitSurface(this->text_bg_middle, NULL, where, (SDL_Rect){bg_x_start + i * this->text_bg_middle->w, bg_y_start, 0,0}); SDL_BlitSurface(this->text_bg_right, NULL, where, (SDL_Rect){bg_x_end, bg_y_start, 0,0}); } void Menu::draw(SDL_Surface *where, int x, int y, int w, int h) { int font_height = TTF_FontHeight(this->p_font); int line_height = (font_height + font_height / 4); int x_start = x; int y_start = y + line_height; SDL_Rect r; int entries_visible = h / line_height - 2; if (this->cur_sel - this->start_entry_visible > entries_visible) { while (this->cur_sel - this->start_entry_visible > entries_visible) { this->start_entry_visible++; if (this->start_entry_visible > this->n_entries) { this->start_entry_visible = 0; break; } } } else if ( this->cur_sel < this->start_entry_visible ) this->start_entry_visible = this->cur_sel; for (i = this->start_entry_visible; i <= this->start_entry_visible + entries_visible; i++) { const char *msg = this->pp_msgs[i]; int cur_y; if (i >= this->n_entries) break; cur_y = (i - this->start_entry_visible) * line_height; /* Draw the background for the selected entry */ if (this->cur_sel == i) { int tw, th; TTF_SizeText(this->font, msg, &tw, &th); this->highlightBackground(x_start, cur_y, tw, th); } /* And print the text on top */ this->printText(msg, this->text_color, x_start, y_start + y, w); if (IS_SUBMENU(msg)) { submenu_t *p_submenu = this->findSubmenu(i); int n_pipe = 0; int total_chars = 0; int tw, th, tw_first, th_first; int n_chars; char *p; int n; for (n = 0; msg[n] != '\0'; n++) { if (msg[n] != '|') continue; /* msg[n] == '|' */ /* Count the number of chars until next pipe */ for (n_chars = 1; msg[n+n_chars] && msg[n+n_chars] != '|'; n_chars++); total_chars += n_chars; n_pipe++; /* Found the selection yet? */ if (p_submenu->sel == n_pipe-1) break; } p = xmalloc(total_chars + 1); strncpy(p, msg, total_chars); TTF_SizeText(this->font, p, &tw_first, &th_first); strncpy(p, msg + n, n_chars); TTF_SizeText(this->font, p, &tw, &th); this->highlightBackground(x_start + tw_first, cur_y, tw, th); free(p); } } } int Menu::getNextEntry(int dy) { if (v + dy < 0) return this->n_entries - 1; if (v + dy > this->n_entries - 1) return 0; return v + dy; } submenu_t *Menu::findSubmenu(int index) { int i; for (i = 0; i < this->n_submenus; i++) { if (this->p_submenus[i].index == index) return &this->p_submenus[i]; } return NULL; } void Menu::selectOne(int which) { if (which >= this->n_entries) which = 0; this->cur_sel = which; if (this->pp_msgs[this->cur_sel][0] == ' ' || this->pp_msgs[this->cur_sel][0] == '#' || IS_SUBMENU(this->pp_msgs[this->cur_sel])) this->selectNext(0, 1); } void Menu::selectNext(int dx, int dy) { int next; char buffer[256]; this->cur_sel = this->getNextEntry(dy); next = this->getNextEntry(dy + 1); /* We want to skip this for some reason */ if (this->pp_msgs[this->cur_sel][0] == ' ' || this->pp_msgs[this->cur_sel][0] == '#' || IS_SUBMENU(this->pp_msgs[this->cur_sel]) ) { this->selectNext(dx, dy); return; } /* If the next is a submenu */ if (dx != 0 && IS_SUBMENU(this->pp_msgs[next])) { submenu_t *p_submenu = findSubmenu(p_menu, next); panic_if(!p_submenu, "submenu in the menu text but no actual submenu\n"); p_submenu->sel = (p_submenu->sel + dx) < 0 ? p_submenu->n_entries - 1 : (p_submenu->sel + dx) % p_submenu->n_entries; } } void Menu::selectNext(event_t ev) { switch (ev) { case KEY_UP: this->selectNext(0, -1); break; case KEY_DOWN: this->selectNext(0, 1); break; case KEY_LEFT: this->selectNext(-1, 0); break; case KEY_RIGHT: this->selectNext(1, 0); break; default: panic("selectNext(ev) called with event %d\n", ev); } } void Menu::runLogic() { event_t ev; while ( (ev = this->popEvent()) != EVENT_NONE ) { switch (ev) { case KEY_UP: case KEY_DOWN: case KEY_LEFT: case KEY_RIGHT: this->selectNext(ev); break; case KEY_SELECT: this->selectCallback(this->cur_sel); break; case KEY_ESCAPE: this->escapeCallback(this->cur_sel); break; break; case KEY_ENTER: default: break; } } } event_t Menu::popEvent() { event_t out; if (this->ev_head == this->ev_tail) return EVENT_NONE; out = this->event_stack[this->ev_tail]; this->ev_tail = (this->ev_tail + 1) % 8; return out; } void Menu::pushEvent(event_t ev) { /* Push... */ this->event_stack[this->ev_head] = ev; /* ... and update */ this->ev_head = (this->ev_head + 1) % 8; if (this->ev_head == this->ev_tail) this->ev_tail = (this->ev_tail + 1) % 8; } void Menu::pushEvent(SDL_Event *ev) { switch(ev->type) { case SDL_KEYDOWN: switch (ev->key.keysym.sym) { case SDLK_UP: this->pushEvent(KEY_UP); break; case SDLK_DOWN: this->pushEvent(KEY_DOWN); break; case SDLK_LEFT: this->pushEvent(KEY_LEFT); break; case SDLK_RIGHT: this->pushEvent(KEY_RIGHT); break; case SDLK_PAGEDOWN: this->pushEvent(KEY_PAGEDOWN); break; case SDLK_PAGEUP: this->pushEvent(KEY_PAGEUP); break; case SDLK_RETURN: case SDLK_SPACE: this->pushEvent(KEY_SELECT); break; case SDLK_HOME: case SDLK_ESCAPE: this->pushEvent(KEY_ESCAPE); break; default: break; } default: break; } } void Menu::setText(const char *messages) { int submenu; /* Free the old stuff */ this->n_submenus = 0; free(this->p_submenus); free(this->pp_msgs); for (this->n_entries = 0; messages[p_menu->n_entries]; this->n_entries++) { /* Is this a submenu? */ if (IS_SUBMENU(messages[this->n_entries])) { this->n_submenus++; continue; /* Length of submenus is unimportant */ } } this->pp_msgs = (const char **)xmalloc(this->n_entries * sizeof(const char *)); this->p_submenus = (submenu_t *)xmalloc(this->n_submenus * sizeof(submenu_t)); for (int i = 0; i < this->n_entries; i++) this->pp_msgs[i] = xstrdup(messages[i]); submenu = 0; for (int i = 0; i < this->n_entries; i++) { if (IS_SUBMENU(this->pp_msgs[i])) { int n; this->p_submenus[submenu].index = i; this->p_submenus[submenu].sel = 0; this->p_submenus[submenu].n_entries = 0; for (n = 0; this->pp_msgs[i][n] != '\0'; n++) { if (this->pp_msgs[i][n] == '|') this->p_submenus[submenu].n_entries++; } submenu++; } } /* Move selection to the first entry */ this->selectOne(0); } Menu::Menu(TTF_Font *font) { this->setSelectedColor((SDL_Color){0x0,0xff,0xff,0}); this->setTextColor((SDL_Color){0xff,0xff,0xff,0}); this->font = font; this->text_bg_left = NULL; this->text_bg_middle = NULL; this->text_bg_right = NULL; this->pp_msgs = NULL; this->n_entries = 0; this->p_submenus = NULL; this->n_submenus = 0; this->cur_sel = 0; this->mouse_x = -1; this->mouse_y = -1; memset(this->event_stack, 0, sizeof(this->event_stack)); this->ev_head = this->ev_tail = 0; } void Menu::setTextColor(SDL_Color clr) { this->text_selected_color = clr; } Menu::~Menu() { free(this->pp_msgs); free(this->p_submenus); }