git.y1.nz

SameBoy

Accurate GB/GBC emulator
download: https://git.y1.nz/archives/sameboy.tar.gz
README | Files | Log | Refs | LICENSE

SDL/gui.c

      1 #include <SDL.h>
      2 #include <stdbool.h>
      3 #include <stdio.h>
      4 #include <stdint.h>
      5 #include <stdlib.h>
      6 #include <string.h>
      7 #include <dirent.h>
      8 #include <ctype.h>
      9 #include "open_dialog/open_dialog.h"
     10 #include "utils.h"
     11 #include "gui.h"
     12 #include "font.h"
     13 #include "audio/audio.h"
     14 
     15 #ifdef _WIN32
     16 #include <dwmapi.h>
     17 #include "windows_associations.h"
     18 #include <SDL_syswm.h>
     19 #endif
     20 
     21 static const SDL_Color gui_palette[4] = {{8, 24, 16,}, {57, 97, 57,}, {132, 165, 99}, {198, 222, 140}};
     22 static uint32_t gui_palette_native[4];
     23 
     24 SDL_Window *window = NULL;
     25 SDL_Renderer *renderer = NULL;
     26 SDL_Texture *texture = NULL;
     27 SDL_PixelFormat *pixel_format = NULL;
     28 enum pending_command pending_command;
     29 unsigned command_parameter;
     30 char *dropped_state_file = NULL;
     31 
     32 static char **custom_palettes;
     33 static unsigned n_custom_palettes;
     34 
     35 
     36 #ifdef __APPLE__
     37 #define MODIFIER_NAME " " CMD_STRING
     38 #else
     39 #define MODIFIER_NAME CTRL_STRING
     40 #endif
     41 
     42 shader_t shader;
     43 static SDL_Rect rect;
     44 static unsigned factor;
     45 
     46 static SDL_Surface *converted_background = NULL;
     47 
     48 bool screen_manually_resized = false;
     49 
     50 void render_texture(void *pixels,  void *previous)
     51 {
     52     if (renderer) {
     53         if (pixels) {
     54             SDL_UpdateTexture(texture, NULL, pixels, GB_get_screen_width(&gb) * sizeof (uint32_t));
     55         }
     56         SDL_RenderClear(renderer);
     57         SDL_RenderCopy(renderer, texture, NULL, NULL);
     58         SDL_RenderPresent(renderer);
     59     }
     60     else {
     61         glClearColor(0, 0, 0, 1);
     62         glClear(GL_COLOR_BUFFER_BIT);
     63         GB_frame_blending_mode_t mode = configuration.blending_mode;
     64         if (!previous) {
     65             mode = GB_FRAME_BLENDING_MODE_DISABLED;
     66         }
     67         else if (mode == GB_FRAME_BLENDING_MODE_ACCURATE) {
     68             if (GB_is_sgb(&gb)) {
     69                 mode = GB_FRAME_BLENDING_MODE_SIMPLE;
     70             }
     71             else {
     72                 mode = GB_is_odd_frame(&gb)? GB_FRAME_BLENDING_MODE_ACCURATE_ODD : GB_FRAME_BLENDING_MODE_ACCURATE_EVEN;
     73             }
     74         }
     75         render_bitmap_with_shader(&shader, pixels, previous,
     76                                   GB_get_screen_width(&gb), GB_get_screen_height(&gb),
     77                                   rect.x, rect.y, rect.w, rect.h,
     78                                   mode);
     79         SDL_GL_SwapWindow(window);
     80     }
     81 }
     82 
     83 static const char *help[] = {
     84 // Shortcuts
     85 "Keyboard Shortcuts:\n"
     86 " Open menu:        Escape\n"
     87 " Open ROM:          " MODIFIER_NAME "+O\n"
     88 " Reset:             " MODIFIER_NAME "+R\n"
     89 " Pause:             " MODIFIER_NAME "+P\n"
     90 " Save state:    " MODIFIER_NAME "+(0-9)\n"
     91 " Load state:  " MODIFIER_NAME "+" SHIFT_STRING "+(0-9)\n"
     92 " Toggle fullscreen  " MODIFIER_NAME "+F\n"
     93 #ifdef __APPLE__
     94 " Mute/Unmute:     " MODIFIER_NAME "+" SHIFT_STRING "+M\n"
     95 " Save screenshot:   " MODIFIER_NAME "+S\n"
     96 #else
     97 " Mute/Unmute:       " MODIFIER_NAME "+M\n"
     98 " Save screenshot:   F12\n"
     99 #endif
    100 " Toggle channel: " ALT_STRING "+(1-4)\n"
    101 " Break debugger:    " CTRL_STRING "+C",
    102 
    103 // About
    104 "\n"
    105 "SameBoy\n"
    106 "Version " GB_VERSION "\n\n"
    107 "Copyright " COPYRIGHT_STRING " 2015-" GB_COPYRIGHT_YEAR "\n"
    108 "Lior Halphon\n\n"
    109 "Licensed under the Expat\n"
    110 "License, see LICENSE for\n"
    111 "more details."
    112 };
    113 
    114 void update_viewport(void)
    115 {
    116     int win_width, win_height;
    117     SDL_GL_GetDrawableSize(window, &win_width, &win_height);
    118     int logical_width, logical_height;
    119     SDL_GetWindowSize(window, &logical_width, &logical_height);
    120     factor = win_width / logical_width;
    121     
    122     double x_factor = win_width / (double) GB_get_screen_width(&gb);
    123     double y_factor = win_height / (double) GB_get_screen_height(&gb);
    124     
    125     if (configuration.scaling_mode == GB_SDL_SCALING_INTEGER_FACTOR) {
    126         x_factor = (unsigned)(x_factor);
    127         y_factor = (unsigned)(y_factor);
    128     }
    129     
    130     if (configuration.scaling_mode != GB_SDL_SCALING_ENTIRE_WINDOW) {
    131         if (x_factor > y_factor) {
    132             x_factor = y_factor;
    133         }
    134         else {
    135             y_factor = x_factor;
    136         }
    137     }
    138     
    139     unsigned new_width = x_factor * GB_get_screen_width(&gb);
    140     unsigned new_height = y_factor * GB_get_screen_height(&gb);
    141     
    142     rect = (SDL_Rect){(win_width  - new_width) / 2, (win_height - new_height) /2,
    143         new_width, new_height};
    144     
    145     if (renderer) {
    146         SDL_RenderSetViewport(renderer, &rect);
    147     }
    148     else {
    149         glViewport(rect.x, rect.y, rect.w, rect.h);
    150     }
    151 }
    152 
    153 static void rescale_window(void)
    154 {
    155     SDL_SetWindowSize(window, GB_get_screen_width(&gb) * configuration.default_scale, GB_get_screen_height(&gb) * configuration.default_scale);
    156 }
    157 
    158 static void draw_char(uint32_t *buffer, unsigned width, unsigned height, unsigned char ch, uint32_t color, uint32_t *mask_top, uint32_t *mask_bottom)
    159 {
    160     if (ch < ' ' || ch > font_max) {
    161         ch = '?';
    162     }
    163 
    164     // Huge dirty hack
    165     if ((signed char)ch == CHECKBOX_ON_STRING[0] || (signed char)ch == CHECKBOX_OFF_STRING[0]) {
    166         if (color == gui_palette_native[3]) {
    167             color = gui_palette_native[0];
    168         }
    169         else if (color == gui_palette_native[0]) {
    170             color = gui_palette_native[3];
    171             ch = CHECKBOX_OFF_STRING[0];
    172         }
    173     }
    174     
    175     uint8_t *data = &font[(ch - ' ') * GLYPH_WIDTH * GLYPH_HEIGHT];
    176     
    177     for (unsigned y = GLYPH_HEIGHT; y--;) {
    178         for (unsigned x = GLYPH_WIDTH; x--;) {
    179             if (*(data++) && buffer >= mask_top && buffer < mask_bottom) {
    180                 (*buffer) = color;
    181             }
    182             buffer++;
    183         }
    184         buffer += width - GLYPH_WIDTH;
    185     }
    186 }
    187 
    188 static signed scroll = 0;
    189 static void draw_unbordered_text(uint32_t *buffer, unsigned width, unsigned height, unsigned x, signed y, const char *string, uint32_t color, bool is_osd)
    190 {
    191     if (!is_osd) {
    192         y -= scroll;
    193     }
    194     unsigned orig_x = x;
    195     unsigned y_offset = is_osd? 0 : (GB_get_screen_height(&gb) - 144) / 2;
    196     while (*string) {
    197         if (*string == '\n') {
    198             x = orig_x;
    199             y += GLYPH_HEIGHT + 4;
    200             string++;
    201             continue;
    202         }
    203         
    204         if (x > width - GLYPH_WIDTH) {
    205             break;
    206         }
    207         
    208         draw_char(&buffer[(signed)(x + width * y)], width, height, *string, color, &buffer[width * y_offset], &buffer[width * (is_osd? GB_get_screen_height(&gb) : y_offset + 144)]);
    209         x += GLYPH_WIDTH;
    210         string++;
    211     }
    212 }
    213 
    214 void draw_text(uint32_t *buffer, unsigned width, unsigned height, unsigned x, signed y, const char *string, uint32_t color, uint32_t border, bool is_osd)
    215 {
    216     draw_unbordered_text(buffer, width, height, x - 1, y, string, border, is_osd);
    217     draw_unbordered_text(buffer, width, height, x + 1, y, string, border, is_osd);
    218     draw_unbordered_text(buffer, width, height, x, y - 1, string, border, is_osd);
    219     draw_unbordered_text(buffer, width, height, x, y + 1, string, border, is_osd);
    220     draw_unbordered_text(buffer, width, height, x, y, string, color, is_osd);
    221 }
    222 
    223 const char *osd_text = NULL;
    224 unsigned osd_countdown = 0;
    225 unsigned osd_text_lines = 1;
    226 
    227 void show_osd_text(const char *text)
    228 {
    229     osd_text_lines = 1;
    230     osd_text = text;
    231     osd_countdown = 30;
    232     while (*text++) {
    233         if (*text == '\n') {
    234             osd_text_lines++;
    235             osd_countdown += 30;
    236         }
    237     }
    238 }
    239 
    240 
    241 enum style {
    242     STYLE_LEFT,
    243     STYLE_INDENT,
    244     STYLE_CENTER,
    245     STYLE_SELECTION,
    246     STYLE_ARROWS,
    247 };
    248 
    249 static void draw_styled_text(uint32_t *buffer, unsigned width, unsigned height, unsigned y, const char *string, uint32_t color, uint32_t border, enum style style)
    250 {
    251     unsigned x = GLYPH_WIDTH * 2 + (width - 160) / 2;
    252     if (style == STYLE_CENTER || style == STYLE_ARROWS) {
    253         x = width / 2 - (unsigned) strlen(string) * GLYPH_WIDTH / 2;
    254     }
    255     else if (style == STYLE_LEFT) {
    256         x = 6 + (width - 160) / 2;
    257     }
    258     
    259     draw_text(buffer, width, height, x, y, string, color, border, false);
    260     switch (style) {
    261         case STYLE_SELECTION:
    262             draw_text(buffer, width, height, x - GLYPH_WIDTH, y, SELECTION_STRING, color, border, false);
    263             break;
    264         case STYLE_ARROWS:
    265             draw_text(buffer, width, height, x - GLYPH_WIDTH, y, LEFT_ARROW_STRING, color, border, false);
    266             draw_text(buffer, width, height, width - x, y, RIGHT_ARROW_STRING, color, border, false);
    267             break;
    268             
    269         default:
    270             break;
    271     }
    272 }
    273 
    274 struct menu_item {
    275     const char *string;
    276     void (*handler)(unsigned);
    277     const char *(*value_getter)(unsigned);
    278     void (*backwards_handler)(unsigned);
    279 };
    280 static const struct menu_item *current_menu = NULL;
    281 static const struct menu_item *root_menu = NULL;
    282 static unsigned menu_height;
    283 static unsigned scrollbar_size;
    284 static bool mouse_scroling = false;
    285 
    286 static unsigned current_selection = 0;
    287 
    288 static enum {
    289     SHOWING_DROP_MESSAGE,
    290     SHOWING_MENU,
    291     SHOWING_HELP,
    292     WAITING_FOR_KEY,
    293     WAITING_FOR_JBUTTON,
    294     TEXT_INPUT,
    295 } gui_state;
    296 
    297 static char text_input_title[26];
    298 static char text_input_title2[26];
    299 static char text_input[26];
    300 
    301 static void (*text_input_callback)(char ch) = NULL;
    302 
    303 static unsigned joypad_configuration_progress = 0;
    304 static uint8_t joypad_axis_temp;
    305 
    306 static void item_exit(unsigned index)
    307 {
    308     pending_command = GB_SDL_QUIT_COMMAND;
    309 }
    310 
    311 static unsigned current_help_page = 0;
    312 static void item_help(unsigned index)
    313 {
    314     current_help_page = 0;
    315     gui_state = SHOWING_HELP;
    316 }
    317 
    318 static void about(unsigned index)
    319 {
    320     current_help_page = 1;
    321     gui_state = SHOWING_HELP;
    322 }
    323 
    324 static void enter_emulation_menu(unsigned index);
    325 static void enter_graphics_menu(unsigned index);
    326 static void enter_keyboard_menu(unsigned index);
    327 static void enter_joypad_menu(unsigned index);
    328 static void enter_audio_menu(unsigned index);
    329 static void enter_controls_menu(unsigned index);
    330 static void enter_help_menu(unsigned index);
    331 static void enter_options_menu(unsigned index);
    332 static void toggle_audio_recording(unsigned index);
    333 
    334 extern void set_filename(const char *new_filename, typeof(free) *new_free_function);
    335 
    336 static void nop(unsigned index){}
    337 
    338 static void open_rom(unsigned index)
    339 {
    340     char *filename = do_open_rom_dialog();
    341     if (filename) {
    342         set_filename(filename, free);
    343         pending_command = GB_SDL_NEW_FILE_COMMAND;
    344     }
    345 }
    346 
    347 static void cart_swap(unsigned index)
    348 {
    349     char *filename = do_open_rom_dialog();
    350     if (filename) {
    351         set_filename(filename, free);
    352         pending_command = GB_SDL_CART_SWAP_COMMAND;
    353     }
    354 }
    355 
    356 static void recalculate_menu_height(void)
    357 {
    358     menu_height = 24;
    359     scrollbar_size = 0;
    360     if (gui_state == SHOWING_MENU) {
    361         for (const struct menu_item *item = current_menu; item->string; item++) {
    362             menu_height += 12;
    363             if (item->backwards_handler) {
    364                 menu_height += 12;
    365             }
    366         }
    367     }
    368     if (menu_height > 144) {
    369         scrollbar_size = 144 * 140 / menu_height;
    370     }
    371 }
    372 
    373 #if SDL_COMPILEDVERSION < 2014
    374 int SDL_OpenURL(const char *url)
    375 {
    376     char *string = NULL;
    377 #ifdef __APPLE__
    378     asprintf(&string, "open '%s'", url);
    379 #else
    380 #ifdef _WIN32
    381     asprintf(&string, "explorer '%s'", url);
    382 #else
    383     asprintf(&string, "xdg-open '%s'", url);
    384 #endif
    385 #endif
    386     int ret = system(string);
    387     free(string);
    388     return ret;
    389 }
    390 #endif
    391 
    392 static char audio_recording_menu_item[] = "Start Audio Recording";
    393 
    394 static void sponsor(unsigned index)
    395 {
    396     SDL_OpenURL("https://github.com/sponsors/LIJI32");
    397 }
    398 
    399 static void debugger_help(unsigned index)
    400 {
    401     SDL_OpenURL("https://sameboy.github.io/debugger/");
    402 }
    403 
    404 static void return_to_root_menu(unsigned index)
    405 {
    406     current_menu = root_menu;
    407     current_selection = 0;
    408     scroll = 0;
    409     recalculate_menu_height();
    410 }
    411 
    412 #ifdef _WIN32
    413 static void associate_rom_files(unsigned index);
    414 #endif
    415 
    416 static
    417 #ifndef _WIN32
    418 const
    419 #endif
    420 struct menu_item options_menu[] = {
    421     {"Emulation Options", enter_emulation_menu},
    422     {"Graphic Options", enter_graphics_menu},
    423     {"Audio Options", enter_audio_menu},
    424     {"Control Options", enter_controls_menu},
    425 #ifdef _WIN32
    426     {"Associate ROM Files", associate_rom_files},
    427 #endif
    428     {"Back", return_to_root_menu},
    429     {NULL,}
    430 };
    431 
    432 #ifdef _WIN32
    433 static void associate_rom_files(unsigned index)
    434 {
    435     if (GB_do_windows_association()) {
    436         options_menu[index].string = "ROM Files Associated";
    437     }
    438     else {
    439         options_menu[index].string = "Files Association Failed";
    440     }
    441 }
    442 #endif
    443 
    444 static void enter_options_menu(unsigned index)
    445 {
    446     current_menu = options_menu;
    447     current_selection = 0;
    448     scroll = 0;
    449     recalculate_menu_height();
    450 }
    451 
    452 static const GB_cheat_t *current_cheat = NULL;
    453 
    454 extern struct menu_item modify_cheat_menu[];
    455 
    456 static void save_cheats(void)
    457 {
    458     extern char *filename;
    459     size_t path_length = strlen(filename);
    460     char cheat_path[path_length + 5];
    461     replace_extension(filename, path_length, cheat_path, ".cht");
    462     GB_save_cheats(&gb, cheat_path);
    463 }
    464 
    465 static void rename_callback(char ch)
    466 {
    467     if (ch == '\b' && text_input[0]) {
    468         text_input[strlen(text_input) - 1] = 0;
    469         return;
    470     }
    471     if (ch == '\n') {
    472         GB_update_cheat(&gb,
    473                         current_cheat,
    474                         text_input,
    475                         current_cheat->address,
    476                         current_cheat->bank,
    477                         current_cheat->value,
    478                         current_cheat->old_value,
    479                         current_cheat->use_old_value,
    480                         current_cheat->enabled);
    481         modify_cheat_menu[0].string = current_cheat->description;
    482         gui_state = SHOWING_MENU;
    483         save_cheats();
    484         SDL_StopTextInput();
    485         return;
    486     }
    487     if (ch < ' ') return;
    488     size_t len = strlen(text_input);
    489     if (len < 21) {
    490         text_input[len] = ch;
    491         text_input[len + 1] = 0;
    492     }
    493 }
    494 
    495 static void rename_cheat(unsigned index)
    496 {
    497     strcpy(text_input_title, "Rename Cheat");
    498     text_input_title2[0] = 0;
    499     memcpy(text_input, current_cheat->description, 24);
    500     text_input[24] = 0;
    501     gui_state = TEXT_INPUT;
    502     text_input_callback = rename_callback;
    503     SDL_StartTextInput();
    504     GB_update_cheat(&gb,
    505                     current_cheat,
    506                     current_cheat->description,
    507                     current_cheat->address,
    508                     current_cheat->bank,
    509                     current_cheat->value,
    510                     current_cheat->old_value,
    511                     current_cheat->use_old_value,
    512                     current_cheat->enabled);
    513     save_cheats();
    514 }
    515 
    516 
    517 static void toggle_cheat(unsigned index)
    518 {
    519     GB_update_cheat(&gb,
    520                     current_cheat,
    521                     current_cheat->description,
    522                     current_cheat->address,
    523                     current_cheat->bank,
    524                     current_cheat->value,
    525                     current_cheat->old_value,
    526                     current_cheat->use_old_value,
    527                     !current_cheat->enabled);
    528     save_cheats();
    529 }
    530 
    531 static const char *active_cheat_checkbox(unsigned index)
    532 {
    533     return current_cheat->enabled? CHECKBOX_ON_STRING : CHECKBOX_OFF_STRING;
    534 }
    535 
    536 static const char *get_cheat_address(unsigned index)
    537 {
    538     static char ret[12];
    539     if (current_cheat->bank == GB_CHEAT_ANY_BANK) {
    540         sprintf(ret, "$%04X", current_cheat->address);
    541     }
    542     else {
    543         sprintf(ret, "$%02X:$%04X", current_cheat->bank, current_cheat->address);
    544     }
    545     
    546     return ret;
    547 }
    548 
    549 static void change_cheat_address_callback(char ch)
    550 {
    551     if (ch == '\b' && text_input[1]) {
    552         size_t len = strlen(text_input);
    553         text_input[len - 1] = 0;
    554         if (text_input[len - 2] == ':') {
    555             text_input[len - 2] = 0;
    556         }
    557         return;
    558     }
    559     if (ch == '\n') {
    560         uint16_t bank = GB_CHEAT_ANY_BANK;
    561         uint16_t address = 0;
    562         
    563         const char *s = text_input + 1;
    564         while (*s) {
    565             if (*s == ':') {
    566                 bank = address;
    567                 address = 0;
    568             }
    569             else if (*s >= '0' && *s <= '9'){
    570                 address *= 0x10;
    571                 address += *s - '0';
    572             }
    573             else if (*s >= 'A' && *s <= 'F'){
    574                 address *= 0x10;
    575                 address += *s - 'A' + 10;
    576             }
    577             s++;
    578         }
    579         
    580         GB_update_cheat(&gb,
    581                         current_cheat,
    582                         current_cheat->description,
    583                         address,
    584                         bank,
    585                         current_cheat->value,
    586                         current_cheat->old_value,
    587                         current_cheat->use_old_value,
    588                         current_cheat->enabled);
    589         save_cheats();
    590         gui_state = SHOWING_MENU;
    591         SDL_StopTextInput();
    592         return;
    593     }
    594     size_t len = strlen(text_input);
    595     if (len >= 12) return;
    596     if (ch == ':' && (len >= 2) && !strchr(text_input, ':')) {
    597         text_input[len] = ':';
    598         text_input[len + 1] = '$';
    599         text_input[len + 2] = 0;
    600         return;
    601     }
    602     ch = toupper(ch);
    603     if (!isxdigit(ch)) return;
    604     
    605     unsigned digit_count = 0;
    606     const char *s = text_input + 1;
    607     while (*s) {
    608         if (*s == ':') {
    609             s += 2;
    610             digit_count = 0;
    611         }
    612         else {
    613             s++;
    614             digit_count++;
    615         }
    616     }
    617     
    618     if (digit_count == 4) return;
    619 
    620     text_input[len] = ch;
    621     text_input[len + 1] = 0;
    622 }
    623 
    624 static void change_cheat_address(unsigned index)
    625 {
    626     strcpy(text_input_title, "Enter Cheat Address");
    627     text_input_title2[0] = 0;
    628     strcpy(text_input, get_cheat_address(0));
    629     gui_state = TEXT_INPUT;
    630     text_input_callback = change_cheat_address_callback;
    631     SDL_StartTextInput();
    632 }
    633 
    634 static const char *get_cheat_value(unsigned index)
    635 {
    636     static char ret[4];
    637     sprintf(ret, "$%02X", current_cheat->value);
    638     
    639     return ret;
    640 }
    641 
    642 static void change_cheat_value_callback(char ch)
    643 {
    644     if (ch == '\b' && text_input[1]) {
    645         size_t len = strlen(text_input);
    646         text_input[len - 1] = 0;
    647         return;
    648     }
    649     if (ch == '\n') {
    650         uint8_t value = 0;
    651         
    652         const char *s = text_input + 1;
    653         while (*s) {
    654             if (*s >= '0' && *s <= '9'){
    655                 value *= 0x10;
    656                 value += *s - '0';
    657             }
    658             else if (*s >= 'A' && *s <= 'F'){
    659                 value *= 0x10;
    660                 value += *s - 'A' + 10;
    661             }
    662             s++;
    663         }
    664         
    665         GB_update_cheat(&gb,
    666                         current_cheat,
    667                         current_cheat->description,
    668                         current_cheat->address,
    669                         current_cheat->bank,
    670                         value,
    671                         current_cheat->old_value,
    672                         current_cheat->use_old_value,
    673                         current_cheat->enabled);
    674         save_cheats();
    675         gui_state = SHOWING_MENU;
    676         SDL_StopTextInput();
    677         return;
    678     }
    679     if (!isxdigit(ch)) return;
    680     ch = toupper(ch);
    681     size_t len = strlen(text_input);
    682     if (len == 3) {
    683         text_input[1] = text_input[2];
    684         text_input[2] = ch;
    685         return;
    686     }
    687         
    688     text_input[len] = ch;
    689     text_input[len + 1] = 0;
    690 }
    691 
    692 static void change_cheat_value(unsigned index)
    693 {
    694     strcpy(text_input_title, "Enter Cheat Value");
    695     text_input_title2[0] = 0;
    696     strcpy(text_input, get_cheat_value(0));
    697     gui_state = TEXT_INPUT;
    698     text_input_callback = change_cheat_value_callback;
    699     SDL_StartTextInput();
    700 }
    701 
    702 static const char *get_cheat_old_value(unsigned index)
    703 {
    704     if (!current_cheat->use_old_value) {
    705         return "Any";
    706     }
    707     static char ret[4];
    708     sprintf(ret, "$%02X", current_cheat->old_value);
    709     
    710     return ret;
    711 }
    712 
    713 static void change_cheat_old_value_callback(char ch)
    714 {
    715     if (ch == '\b' && strcmp(text_input, "Any") != 0) {
    716         size_t len = strlen(text_input);
    717         if (len == 2) {
    718             strcpy(text_input, "Any");
    719             return;
    720         }
    721         text_input[len - 1] = 0;
    722         return;
    723     }
    724     if (ch == '\n') {
    725         uint8_t value = 0;
    726         
    727         bool use_old_value = strcmp(text_input, "Any") != 0;
    728         if (use_old_value) {
    729             const char *s = text_input + 1;
    730             while (*s) {
    731                 if (*s >= '0' && *s <= '9'){
    732                     value *= 0x10;
    733                     value += *s - '0';
    734                 }
    735                 else if (*s >= 'A' && *s <= 'F'){
    736                     value *= 0x10;
    737                     value += *s - 'A' + 10;
    738                 }
    739                 s++;
    740             }
    741         }
    742         
    743         GB_update_cheat(&gb,
    744                         current_cheat,
    745                         current_cheat->description,
    746                         current_cheat->address,
    747                         current_cheat->bank,
    748                         current_cheat->value,
    749                         value,
    750                         use_old_value,
    751                         current_cheat->enabled);
    752         save_cheats();
    753         gui_state = SHOWING_MENU;
    754         SDL_StopTextInput();
    755         return;
    756     }
    757     if (!isxdigit(ch)) return;
    758     ch = toupper(ch);
    759     if (strcmp(text_input, "Any") == 0) {
    760         strcpy(text_input, "$");
    761     }
    762     size_t len = strlen(text_input);
    763     if (len == 3) {
    764         text_input[1] = text_input[2];
    765         text_input[2] = ch;
    766         return;
    767     }
    768     
    769     text_input[len] = ch;
    770     text_input[len + 1] = 0;
    771 }
    772 
    773 static void change_cheat_old_value(unsigned index)
    774 {
    775     strcpy(text_input_title, "Enter Cheat Old Value");
    776     text_input_title2[0] = 0;
    777     strcpy(text_input, get_cheat_old_value(0));
    778     gui_state = TEXT_INPUT;
    779     text_input_callback = change_cheat_old_value_callback;
    780     SDL_StartTextInput();
    781 }
    782 
    783 static void enter_cheats_menu(unsigned index);
    784 
    785 static void delete_cheat(unsigned index)
    786 {
    787     GB_remove_cheat(&gb, current_cheat);
    788     save_cheats();
    789     enter_cheats_menu(0);
    790 }
    791 
    792 struct menu_item modify_cheat_menu[] = {
    793     {"", rename_cheat},
    794     {"Enable", toggle_cheat, active_cheat_checkbox},
    795     {"Address:", change_cheat_address, get_cheat_address},
    796     {"Value:", change_cheat_value, get_cheat_value},
    797     {"Old Value:", change_cheat_old_value, get_cheat_old_value},
    798     {"Delete Cheat", delete_cheat},
    799     {"Back", enter_cheats_menu},
    800     {NULL,}
    801 };
    802 
    803 static void toggle_cheats(unsigned index)
    804 {
    805     GB_set_cheats_enabled(&gb, !GB_cheats_enabled(&gb));
    806 }
    807 
    808 static void add_cheat(unsigned index)
    809 {
    810     current_cheat = GB_add_cheat(&gb, "New Cheat", 0, 0, 0, 0, false, true);
    811     modify_cheat_menu[0].string = current_cheat->description;
    812     current_menu = modify_cheat_menu;
    813     current_selection = 0;
    814     scroll = 0;
    815     save_cheats();
    816 }
    817 
    818 static void import_cheat_callback(char ch)
    819 {
    820     if (ch == '\b' && text_input[0]) {
    821         size_t len = strlen(text_input);
    822         text_input[len - 1] = 0;
    823         return;
    824     }
    825     if (ch == '\n') {
    826         if (!text_input[0]) {
    827             gui_state = SHOWING_MENU;
    828             SDL_StopTextInput();
    829             return;
    830         }
    831         
    832         current_cheat = GB_import_cheat(&gb, text_input, "Imported Cheat", true);
    833         if (current_cheat) {
    834             gui_state = SHOWING_MENU;
    835             modify_cheat_menu[0].string = current_cheat->description;
    836             current_menu = modify_cheat_menu;
    837             current_selection = 0;
    838             scroll = 0;
    839             save_cheats();
    840             SDL_StopTextInput();
    841             return;
    842         }
    843         
    844         strcpy(text_input_title, "Invalid Code.");
    845         strcpy(text_input_title2, "Press Enter to Cancel");
    846         text_input[0] = 0;
    847         return;
    848     }
    849     if (ch != '-' && !isxdigit(ch)) return;
    850     ch = toupper(ch);
    851     size_t len = strlen(text_input);
    852     if (len >= 12) {
    853         return;
    854     }
    855     
    856     text_input[len] = ch;
    857     text_input[len + 1] = 0;
    858     if (text_input_title[0] != 'E') {
    859         strcpy(text_input_title, "Enter a GameShark");
    860         strcpy(text_input_title2, "or Game Genie Code");
    861     }
    862 
    863 }
    864 
    865 
    866 static void import_cheat(unsigned index)
    867 {
    868     strcpy(text_input_title, "Enter a GameShark");
    869     strcpy(text_input_title2, "or Game Genie Code");
    870     text_input[0] = 0;
    871     gui_state = TEXT_INPUT;
    872     text_input_callback = import_cheat_callback;
    873     save_cheats();
    874     SDL_StartTextInput();
    875 }
    876 
    877 static void modify_cheat(unsigned index)
    878 {
    879     const GB_cheat_t *const *cheats = GB_get_cheats(&gb, NULL);
    880     current_cheat = cheats[index - 3];
    881     modify_cheat_menu[0].string = current_cheat->description;
    882     current_menu = modify_cheat_menu;
    883     current_selection = 0;
    884     scroll = 0;
    885 }
    886 
    887 static const char *checkbox_for_cheat(unsigned index)
    888 {
    889     const GB_cheat_t *const *cheats = GB_get_cheats(&gb, NULL);
    890     return cheats[index - 3]->enabled? CHECKBOX_ON_STRING : CHECKBOX_OFF_STRING;
    891 }
    892 
    893 static const char *cheats_global_checkbox(unsigned index)
    894 {
    895     return GB_cheats_enabled(&gb)? CHECKBOX_ON_STRING : CHECKBOX_OFF_STRING;
    896 }
    897 
    898 static void enter_cheats_menu(unsigned index)
    899 {
    900     struct menu_item *cheats_menu = NULL;
    901     if (cheats_menu) {
    902         free(cheats_menu);
    903     }
    904     size_t cheat_count;
    905     const GB_cheat_t *const *cheats = GB_get_cheats(&gb, &cheat_count);
    906     cheats_menu = calloc(cheat_count + 5, sizeof(struct menu_item));
    907     cheats_menu[0] = (struct menu_item){"New Cheat", add_cheat};
    908     cheats_menu[1] = (struct menu_item){"Import Cheat", import_cheat};
    909     cheats_menu[2] = (struct menu_item){"Enable Cheats", toggle_cheats, cheats_global_checkbox};
    910     for (size_t i = 0; i < cheat_count; i++) {
    911         cheats_menu[i + 3] = (struct menu_item){cheats[i]->description, modify_cheat, checkbox_for_cheat};
    912     }
    913     cheats_menu[cheat_count + 3] = (struct menu_item){"Back", return_to_root_menu};
    914     current_menu = cheats_menu;
    915     current_selection = 0;
    916     scroll = 0;
    917     recalculate_menu_height();
    918 }
    919 
    920 static const struct menu_item paused_menu[] = {
    921     {"Resume", NULL},
    922     {"Open ROM", open_rom},
    923     {"Hot Swap Cartridge", cart_swap},
    924     {"Options", enter_options_menu},
    925     {"Cheats", enter_cheats_menu},
    926     {audio_recording_menu_item, toggle_audio_recording},
    927     {"Help & About", enter_help_menu},
    928     {"Sponsor SameBoy", sponsor},
    929     {"Quit SameBoy", item_exit},
    930     {NULL,}
    931 };
    932 
    933 static struct menu_item nonpaused_menu[sizeof(paused_menu) / sizeof(paused_menu[0]) - 3];
    934 
    935 static void __attribute__((constructor)) build_nonpaused_menu(void)
    936 {
    937     const struct menu_item *in = paused_menu;
    938     struct menu_item *out = nonpaused_menu;
    939     while (in->string) {
    940         if (in->handler == NULL || in->handler == cart_swap || in->handler == enter_cheats_menu) {
    941             in++;
    942             continue;
    943         }
    944         *out = *in;
    945         out++;
    946         in++;
    947     }
    948 }
    949 
    950 static const struct menu_item help_menu[] = {
    951     {"Shortcuts", item_help},
    952     {"Debugger Help", debugger_help},
    953     {"About SameBoy", about},
    954     {"Back", return_to_root_menu},
    955     {NULL,}
    956 };
    957 
    958 static void enter_help_menu(unsigned index)
    959 {
    960     current_menu = help_menu;
    961     current_selection = 0;
    962     scroll = 0;
    963     recalculate_menu_height();
    964 }
    965 
    966 
    967 static void cycle_model(unsigned index)
    968 {
    969     switch (configuration.model) {
    970         case MODEL_DMG:  configuration.model = MODEL_MGB;  break;
    971         case MODEL_MGB:  configuration.model = MODEL_SGB;  break;
    972         case MODEL_SGB:  configuration.model = MODEL_CGB;  break;
    973         case MODEL_CGB:  configuration.model = MODEL_AGB;  break;
    974         case MODEL_AGB:  configuration.model = MODEL_AUTO; break;
    975         case MODEL_AUTO: configuration.model = MODEL_DMG;  break;
    976         default: configuration.model = MODEL_AUTO;
    977     }
    978     pending_command = GB_SDL_RESET_COMMAND;
    979 }
    980 
    981 static void cycle_model_backwards(unsigned index)
    982 {
    983     switch (configuration.model) {
    984         case MODEL_MGB:  configuration.model = MODEL_DMG;  break;
    985         case MODEL_SGB:  configuration.model = MODEL_MGB;  break;
    986         case MODEL_CGB:  configuration.model = MODEL_SGB;  break;
    987         case MODEL_AGB:  configuration.model = MODEL_CGB;  break;
    988         case MODEL_AUTO: configuration.model = MODEL_AGB;  break;
    989         case MODEL_DMG:  configuration.model = MODEL_AUTO; break;
    990         default: configuration.model = MODEL_AUTO;
    991     }
    992     pending_command = GB_SDL_RESET_COMMAND;
    993 }
    994 
    995 static const char *current_model_string(unsigned index)
    996 {
    997     return GB_inline_const(const char *[], {"Game Boy", "Game Boy Color", "Game Boy Advance", "Super Game Boy", "Game Boy Pocket", "Pick Automatically"})
    998         [configuration.model];
    999 }
   1000 
   1001 static void cycle_cgb_revision(unsigned index)
   1002 {
   1003     
   1004     if (configuration.cgb_revision == GB_MODEL_CGB_E - GB_MODEL_CGB_0) {
   1005         configuration.cgb_revision = 0;
   1006     }
   1007     else {
   1008         configuration.cgb_revision++;
   1009     }
   1010     pending_command = GB_SDL_RESET_COMMAND;
   1011 }
   1012 
   1013 static void cycle_cgb_revision_backwards(unsigned index)
   1014 {
   1015     if (configuration.cgb_revision == 0) {
   1016         configuration.cgb_revision = GB_MODEL_CGB_E - GB_MODEL_CGB_0;
   1017     }
   1018     else {
   1019         configuration.cgb_revision--;
   1020     }
   1021     pending_command = GB_SDL_RESET_COMMAND;
   1022 }
   1023 
   1024 static const char *current_cgb_revision_string(unsigned index)
   1025 {
   1026     return GB_inline_const(const char *[], {
   1027         "CPU CGB 0",
   1028         "CPU CGB A",
   1029         "CPU CGB B",
   1030         "CPU CGB C",
   1031         "CPU CGB D",
   1032         "CPU CGB E",
   1033     })
   1034     [configuration.cgb_revision];
   1035 }
   1036 
   1037 static void cycle_sgb_revision(unsigned index)
   1038 {
   1039     
   1040     configuration.sgb_revision++;
   1041     if (configuration.sgb_revision == SGB_MAX) {
   1042         configuration.sgb_revision = 0;
   1043     }
   1044     pending_command = GB_SDL_RESET_COMMAND;
   1045 }
   1046 
   1047 static void cycle_sgb_revision_backwards(unsigned index)
   1048 {
   1049     if (configuration.sgb_revision == 0) {
   1050         configuration.sgb_revision = SGB_MAX;
   1051     }
   1052     configuration.sgb_revision--;
   1053     pending_command = GB_SDL_RESET_COMMAND;
   1054 }
   1055 
   1056 static const char *current_sgb_revision_string(unsigned index)
   1057 {
   1058     return GB_inline_const(const char *[], {"Super Game Boy NTSC", "Super Game Boy PAL", "Super Game Boy 2"})
   1059         [configuration.sgb_revision];
   1060 }
   1061 
   1062 static const uint32_t rewind_lengths[] = {0, 10, 30, 60, 60 * 2, 60 * 5, 60 * 10};
   1063 static const char *rewind_strings[] = {"Disabled",
   1064                                        "10 Seconds",
   1065                                        "30 Seconds",
   1066                                        "1 Minute",
   1067                                        "2 Minutes",
   1068                                        "5 Minutes",
   1069                                        "10 Minutes",
   1070 };
   1071 
   1072 static void cycle_rewind(unsigned index)
   1073 {
   1074     for (unsigned i = 0; i < sizeof(rewind_lengths) / sizeof(rewind_lengths[0]) - 1; i++) {
   1075         if (configuration.rewind_length == rewind_lengths[i]) {
   1076             configuration.rewind_length = rewind_lengths[i + 1];
   1077             GB_set_rewind_length(&gb, configuration.rewind_length);
   1078             return;
   1079         }
   1080     }
   1081     configuration.rewind_length = rewind_lengths[0];
   1082     GB_set_rewind_length(&gb, configuration.rewind_length);
   1083 }
   1084 
   1085 static void cycle_rewind_backwards(unsigned index)
   1086 {
   1087     for (unsigned i = 1; i < sizeof(rewind_lengths) / sizeof(rewind_lengths[0]); i++) {
   1088         if (configuration.rewind_length == rewind_lengths[i]) {
   1089             configuration.rewind_length = rewind_lengths[i - 1];
   1090             GB_set_rewind_length(&gb, configuration.rewind_length);
   1091             return;
   1092         }
   1093     }
   1094     configuration.rewind_length = rewind_lengths[sizeof(rewind_lengths) / sizeof(rewind_lengths[0]) - 1];
   1095     GB_set_rewind_length(&gb, configuration.rewind_length);
   1096 }
   1097 
   1098 static const char *current_rewind_string(unsigned index)
   1099 {
   1100     for (unsigned i = 0; i < sizeof(rewind_lengths) / sizeof(rewind_lengths[0]); i++) {
   1101         if (configuration.rewind_length == rewind_lengths[i]) {
   1102             return rewind_strings[i];
   1103         }
   1104     }
   1105     return "Custom";
   1106 }
   1107 
   1108 static const char *current_bootrom_string(unsigned index)
   1109 {
   1110     if (!configuration.bootrom_path[0]) {
   1111         return "Built-in Boot ROMs";
   1112     }
   1113     size_t length = strlen(configuration.bootrom_path);
   1114     static char ret[24] = {0,};
   1115     if (length <= 23) {
   1116         strcpy(ret, configuration.bootrom_path);
   1117     }
   1118     else {
   1119         memcpy(ret, configuration.bootrom_path, 11);
   1120         memcpy(ret + 12, configuration.bootrom_path + length - 11, 11);
   1121     }
   1122     for (unsigned i = 0; i < 24; i++) {
   1123         if (ret[i] < 0) {
   1124             ret[i] = MOJIBAKE_STRING[0];
   1125         }
   1126     }
   1127     if (length > 23) {
   1128         ret[11] = ELLIPSIS_STRING[0];
   1129     }
   1130     return ret;
   1131 }
   1132 
   1133 static void toggle_bootrom(unsigned index)
   1134 {
   1135     if (configuration.bootrom_path[0]) {
   1136         configuration.bootrom_path[0] = 0;
   1137     }
   1138     else {
   1139         char *folder = do_open_folder_dialog();
   1140         if (!folder) return;
   1141         if (strlen(folder) < sizeof(configuration.bootrom_path) - 1) {
   1142             strcpy(configuration.bootrom_path, folder);
   1143         }
   1144         free(folder);
   1145     }
   1146 }
   1147 
   1148 static void toggle_rtc_mode(unsigned index)
   1149 {
   1150     configuration.rtc_mode = !configuration.rtc_mode;
   1151 }
   1152 
   1153 static const char *current_rtc_mode_string(unsigned index)
   1154 {
   1155     switch (configuration.rtc_mode) {
   1156         case GB_RTC_MODE_SYNC_TO_HOST: return "Sync to System Clock";
   1157         case GB_RTC_MODE_ACCURATE: return "Accurate";
   1158     }
   1159     return "";
   1160 }
   1161 
   1162 static void cycle_agb_revision(unsigned index)
   1163 {
   1164     
   1165     configuration.agb_revision ^= GB_MODEL_GBP_BIT;
   1166     pending_command = GB_SDL_RESET_COMMAND;
   1167 }
   1168 
   1169 static const char *current_agb_revision_string(unsigned index)
   1170 {
   1171     if (configuration.agb_revision == GB_MODEL_GBP_A) {
   1172         return "CPU AGB A (GBP)";
   1173     }
   1174     return "CPU AGB A (AGB)";
   1175 }
   1176 
   1177 static void cycle_turbo_cap(unsigned index)
   1178 {
   1179     
   1180     if (configuration.turbo_cap >= 16) { // 400%
   1181         configuration.turbo_cap = 0; // uncapped
   1182     }
   1183     else if (configuration.turbo_cap == 0) { // uncapped
   1184         configuration.turbo_cap = 6; // 150%
   1185     }
   1186     else {
   1187         configuration.turbo_cap++;
   1188     }
   1189 }
   1190 
   1191 static void cycle_turbo_cap_backwards(unsigned index)
   1192 {
   1193     
   1194     if (configuration.turbo_cap == 0) { // uncapped
   1195         configuration.turbo_cap = 16; // 400%
   1196     }
   1197     else if (configuration.turbo_cap == 6) { // 150%
   1198         configuration.turbo_cap = 0; // uncapped
   1199     }
   1200     else {
   1201         configuration.turbo_cap--;
   1202     }
   1203 }
   1204 
   1205 static const char *current_turbo_cap_string(unsigned index)
   1206 {
   1207     if (configuration.turbo_cap == 0) {
   1208         return "Uncapped";
   1209     }
   1210     static char ret[5];
   1211     snprintf(ret, sizeof(ret), "%d%%", configuration.turbo_cap * 25);
   1212     return ret;
   1213 }
   1214 
   1215 
   1216 static const struct menu_item emulation_menu[] = {
   1217     {"Emulated Model:", cycle_model, current_model_string, cycle_model_backwards},
   1218     {"SGB Revision:", cycle_sgb_revision, current_sgb_revision_string, cycle_sgb_revision_backwards},
   1219     {"GBC Revision:", cycle_cgb_revision, current_cgb_revision_string, cycle_cgb_revision_backwards},
   1220     {"GBA Revision:", cycle_agb_revision, current_agb_revision_string, cycle_agb_revision},
   1221     {"Boot ROMs Folder:", toggle_bootrom, current_bootrom_string, toggle_bootrom},
   1222     {"Rewind Length:", cycle_rewind, current_rewind_string, cycle_rewind_backwards},
   1223     {"Real Time Clock:", toggle_rtc_mode, current_rtc_mode_string, toggle_rtc_mode},
   1224     {"Turbo speed cap:", cycle_turbo_cap, current_turbo_cap_string, cycle_turbo_cap_backwards},
   1225     {"Back", enter_options_menu},
   1226     {NULL,}
   1227 };
   1228 
   1229 static void enter_emulation_menu(unsigned index)
   1230 {
   1231     current_menu = emulation_menu;
   1232     current_selection = 0;
   1233     scroll = 0;
   1234     recalculate_menu_height();
   1235 }
   1236 
   1237 static const char *current_scaling_mode(unsigned index)
   1238 {
   1239     return GB_inline_const(const char *[], {"Fill Entire Window", "Retain Aspect Ratio", "Retain Integer Factor"})
   1240         [configuration.scaling_mode];
   1241 }
   1242 
   1243 static const char *current_default_scale(unsigned index)
   1244 {
   1245     return GB_inline_const(const char *[], {"1x", "2x", "3x", "4x", "5x", "6x", "7x", "8x"})
   1246         [configuration.default_scale - 1];
   1247 }
   1248 
   1249 const char *current_color_correction_mode(unsigned index)
   1250 {
   1251     return GB_inline_const(const char *[], {"Disabled", "Correct Color Curves", "Modern - Balanced", "Modern - Boost Contrast", "Reduce Contrast", "Harsh Reality", "Modern - Accurate"})
   1252         [configuration.color_correction_mode];
   1253 }
   1254 
   1255 const char *current_color_temperature(unsigned index)
   1256 {
   1257     static char ret[22];
   1258     strcpy(ret, SLIDER_STRING);
   1259     ret[configuration.color_temperature] = SELECTED_SLIDER_STRING[configuration.color_temperature];
   1260     return ret;
   1261 }
   1262 
   1263 
   1264 const char *current_palette(unsigned index)
   1265 {
   1266     if (configuration.dmg_palette == 4) {
   1267         return configuration.dmg_palette_name;
   1268     }
   1269     return GB_inline_const(const char *[], {"Greyscale", "Lime (Game Boy)", "Olive (Pocket)", "Teal (Light)"})
   1270         [configuration.dmg_palette];
   1271 }
   1272 
   1273 const char *current_border_mode(unsigned index)
   1274 {
   1275     return GB_inline_const(const char *[], {"SGB Only", "Never", "Always"})
   1276         [configuration.border_mode];
   1277 }
   1278 
   1279 static void cycle_scaling(unsigned index)
   1280 {
   1281     configuration.scaling_mode++;
   1282     if (configuration.scaling_mode == GB_SDL_SCALING_MAX) {
   1283         configuration.scaling_mode = 0;
   1284     }
   1285     update_viewport();
   1286     render_texture(NULL, NULL);
   1287 }
   1288 
   1289 static void cycle_scaling_backwards(unsigned index)
   1290 {
   1291     if (configuration.scaling_mode == 0) {
   1292         configuration.scaling_mode = GB_SDL_SCALING_MAX - 1;
   1293     }
   1294     else {
   1295         configuration.scaling_mode--;
   1296     }
   1297     update_viewport();
   1298     render_texture(NULL, NULL);
   1299     screen_manually_resized = false;
   1300 }
   1301 
   1302 static void cycle_default_scale(unsigned index)
   1303 {
   1304     if (configuration.default_scale == GB_SDL_DEFAULT_SCALE_MAX) {
   1305         configuration.default_scale = 1;
   1306     }
   1307     else {
   1308         configuration.default_scale++;
   1309     }
   1310 
   1311     rescale_window();
   1312     update_viewport();
   1313     screen_manually_resized = false;
   1314 }
   1315 
   1316 static void cycle_default_scale_backwards(unsigned index)
   1317 {
   1318     if (configuration.default_scale == 1) {
   1319         configuration.default_scale = GB_SDL_DEFAULT_SCALE_MAX;
   1320     }
   1321     else {
   1322         configuration.default_scale--;
   1323     }
   1324 
   1325     rescale_window();
   1326     update_viewport();
   1327     screen_manually_resized = false;
   1328 }
   1329 
   1330 static void cycle_color_correction(unsigned index)
   1331 {
   1332     if (configuration.color_correction_mode == GB_COLOR_CORRECTION_LOW_CONTRAST) {
   1333         configuration.color_correction_mode = GB_COLOR_CORRECTION_DISABLED;
   1334     }
   1335     else if (configuration.color_correction_mode == GB_COLOR_CORRECTION_MODERN_BALANCED) {
   1336         configuration.color_correction_mode = GB_COLOR_CORRECTION_MODERN_ACCURATE;
   1337     }
   1338     else if (configuration.color_correction_mode == GB_COLOR_CORRECTION_MODERN_ACCURATE) {
   1339         configuration.color_correction_mode = GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST;
   1340     }
   1341     else {
   1342         configuration.color_correction_mode++;
   1343     }
   1344 }
   1345 
   1346 static void cycle_color_correction_backwards(unsigned index)
   1347 {
   1348     if (configuration.color_correction_mode == GB_COLOR_CORRECTION_DISABLED) {
   1349         configuration.color_correction_mode = GB_COLOR_CORRECTION_LOW_CONTRAST;
   1350     }
   1351     else if (configuration.color_correction_mode == GB_COLOR_CORRECTION_MODERN_ACCURATE) {
   1352         configuration.color_correction_mode = GB_COLOR_CORRECTION_MODERN_BALANCED;
   1353     }
   1354     else if (configuration.color_correction_mode == GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST) {
   1355         configuration.color_correction_mode = GB_COLOR_CORRECTION_MODERN_ACCURATE;
   1356     }
   1357     else {
   1358         configuration.color_correction_mode--;
   1359     }
   1360 }
   1361 
   1362 static void decrease_color_temperature(unsigned index)
   1363 {
   1364     if (configuration.color_temperature < 20) {
   1365         configuration.color_temperature++;
   1366     }
   1367 }
   1368 
   1369 static void increase_color_temperature(unsigned index)
   1370 {
   1371     if (configuration.color_temperature > 0) {
   1372         configuration.color_temperature--;
   1373     }
   1374 }
   1375 
   1376 const GB_palette_t *current_dmg_palette(void)
   1377 {
   1378     typedef struct __attribute__ ((packed)) {
   1379         uint32_t magic;
   1380         uint8_t flags;
   1381         struct GB_color_s colors[5];
   1382         int32_t brightness_bias;
   1383         uint32_t hue_bias;
   1384         uint32_t hue_bias_strength;
   1385     } theme_t;
   1386     
   1387     static theme_t theme;
   1388     
   1389     if (configuration.dmg_palette == 4) {
   1390         char *path = resource_path("Palettes");
   1391         sprintf(path + strlen(path), "/%s.sbp", configuration.dmg_palette_name);
   1392         FILE *file = fopen(path, "rb");
   1393         if (!file) return &GB_PALETTE_GREY;
   1394         memset(&theme, 0, sizeof(theme));
   1395         fread(&theme, sizeof(theme), 1, file);
   1396         fclose(file);
   1397 #ifdef GB_BIG_ENDIAN
   1398         theme.magic = __builtin_bswap32(theme.magic);
   1399 #endif
   1400         if (theme.magic != 'SBPL') return &GB_PALETTE_GREY;
   1401         return (GB_palette_t *)&theme.colors;
   1402     }
   1403     
   1404     switch (configuration.dmg_palette) {
   1405         case 1:  return &GB_PALETTE_DMG;
   1406         case 2:  return &GB_PALETTE_MGB;
   1407         case 3:  return &GB_PALETTE_GBL;
   1408         default: return &GB_PALETTE_GREY;
   1409     }
   1410 }
   1411 
   1412 static void update_gui_palette(void)
   1413 {
   1414     const GB_palette_t *palette = current_dmg_palette();
   1415     
   1416     SDL_Color colors[4];
   1417     for (unsigned i = 4; i--; ) {
   1418         gui_palette_native[i] = SDL_MapRGB(pixel_format, palette->colors[i].r, palette->colors[i].g, palette->colors[i].b);
   1419         colors[i].r = palette->colors[i].r;
   1420         colors[i].g = palette->colors[i].g;
   1421         colors[i].b = palette->colors[i].b;
   1422     }
   1423     
   1424     SDL_Surface *background = SDL_LoadBMP(resource_path("background.bmp"));
   1425     
   1426     /* Create a blank background if background.bmp could not be loaded */
   1427     if (!background) {
   1428         background = SDL_CreateRGBSurface(0, 160, 144, 8, 0, 0, 0, 0);
   1429     }
   1430     SDL_SetPaletteColors(background->format->palette, colors, 0, 4);
   1431     converted_background = SDL_ConvertSurface(background, pixel_format, 0);
   1432     SDL_FreeSurface(background);
   1433 }
   1434 
   1435 static void cycle_palette(unsigned index)
   1436 {
   1437     if (configuration.dmg_palette == 3) {
   1438         if (n_custom_palettes == 0) {
   1439             configuration.dmg_palette = 0;
   1440         }
   1441         else {
   1442             configuration.dmg_palette = 4;
   1443             strcpy(configuration.dmg_palette_name, custom_palettes[0]);
   1444         }
   1445     }
   1446     else if (configuration.dmg_palette == 4) {
   1447         bool found = false;
   1448         for (unsigned i = 0; i < n_custom_palettes; i++) {
   1449             if (strcmp(custom_palettes[i], configuration.dmg_palette_name) == 0) {
   1450                 found = true;
   1451                 if (i == n_custom_palettes - 1) {
   1452                     configuration.dmg_palette = 0;
   1453                 }
   1454                 else {
   1455                     strcpy(configuration.dmg_palette_name, custom_palettes[i + 1]);
   1456                 }
   1457                 break;
   1458             }
   1459         }
   1460         if (!found) {
   1461             configuration.dmg_palette = 0;
   1462         }
   1463     }
   1464     else {
   1465         configuration.dmg_palette++;
   1466     }
   1467     configuration.gui_palette_enabled = true;
   1468     update_gui_palette();
   1469 }
   1470 
   1471 static void cycle_palette_backwards(unsigned index)
   1472 {
   1473     if (configuration.dmg_palette == 0) {
   1474         if (n_custom_palettes == 0) {
   1475             configuration.dmg_palette = 3;
   1476         }
   1477         else {
   1478             configuration.dmg_palette = 4;
   1479             strcpy(configuration.dmg_palette_name, custom_palettes[n_custom_palettes - 1]);
   1480         }
   1481     }
   1482     else if (configuration.dmg_palette == 4) {
   1483         for (unsigned i = 0; i < n_custom_palettes; i++) {
   1484             bool found = false;
   1485             if (strcmp(custom_palettes[i], configuration.dmg_palette_name) == 0) {
   1486                 found = true;
   1487                 if (i == 0) {
   1488                     configuration.dmg_palette = 3;
   1489                 }
   1490                 else {
   1491                     strcpy(configuration.dmg_palette_name, custom_palettes[i - 1]);
   1492                 }
   1493                 break;
   1494             }
   1495             if (!found) {
   1496                 configuration.dmg_palette = 3;
   1497             }
   1498         }
   1499     }
   1500     else {
   1501         configuration.dmg_palette--;
   1502     }
   1503     configuration.gui_palette_enabled = true;
   1504     update_gui_palette();
   1505 }
   1506 
   1507 static void cycle_border_mode(unsigned index)
   1508 {
   1509     if (configuration.border_mode == GB_BORDER_ALWAYS) {
   1510         configuration.border_mode = GB_BORDER_SGB;
   1511     }
   1512     else {
   1513         configuration.border_mode++;
   1514     }
   1515 }
   1516 
   1517 static void cycle_border_mode_backwards(unsigned index)
   1518 {
   1519     if (configuration.border_mode == GB_BORDER_SGB) {
   1520         configuration.border_mode = GB_BORDER_ALWAYS;
   1521     }
   1522     else {
   1523         configuration.border_mode--;
   1524     }
   1525 }
   1526 
   1527 extern bool uses_gl(void);
   1528 struct shader_name {
   1529     const char *file_name;
   1530     const char *display_name;
   1531 } shaders[] =
   1532 {
   1533     {"NearestNeighbor", "Nearest Neighbor"},
   1534     {"Bilinear", "Bilinear"},
   1535     {"SmoothBilinear", "Smooth Bilinear"},
   1536     {"MonoLCD", "Monochrome LCD"},
   1537     {"LCD", "LCD Display"},
   1538     {"CRT", "CRT Display"},
   1539     {"FlatCRT", "Flat CRT Display"},
   1540     {"Scale2x", "Scale2x"},
   1541     {"Scale4x", "Scale4x"},
   1542     {"AAScale2x", "Anti-aliased Scale2x"},
   1543     {"AAScale4x", "Anti-aliased Scale4x"},
   1544     {"HQ2x", "HQ2x"},
   1545     {"OmniScale", "OmniScale"},
   1546     {"OmniScaleLegacy", "OmniScale Legacy"},
   1547     {"AAOmniScaleLegacy", "AA OmniScale Legacy"},
   1548 };
   1549 
   1550 static void cycle_filter(unsigned index)
   1551 {
   1552     if (!uses_gl()) return;
   1553     unsigned i = 0;
   1554     for (; i < sizeof(shaders) / sizeof(shaders[0]); i++) {
   1555         if (strcmp(shaders[i].file_name, configuration.filter) == 0) {
   1556             break;
   1557         }
   1558     }
   1559     
   1560 
   1561     i += 1;
   1562     if (i >= sizeof(shaders) / sizeof(shaders[0])) {
   1563         i -= sizeof(shaders) / sizeof(shaders[0]);
   1564     }
   1565     
   1566     strcpy(configuration.filter, shaders[i].file_name);
   1567     free_shader(&shader);
   1568     if (!init_shader_with_name(&shader, configuration.filter)) {
   1569         init_shader_with_name(&shader, "NearestNeighbor");
   1570     }
   1571 }
   1572 
   1573 static void cycle_filter_backwards(unsigned index)
   1574 {
   1575     if (!uses_gl()) return;
   1576     unsigned i = 0;
   1577     for (; i < sizeof(shaders) / sizeof(shaders[0]); i++) {
   1578         if (strcmp(shaders[i].file_name, configuration.filter) == 0) {
   1579             break;
   1580         }
   1581     }
   1582     
   1583     i -= 1;
   1584     if (i >= sizeof(shaders) / sizeof(shaders[0])) {
   1585         i = sizeof(shaders) / sizeof(shaders[0]) - 1;
   1586     }
   1587     
   1588     strcpy(configuration.filter, shaders[i].file_name);
   1589     free_shader(&shader);
   1590     if (!init_shader_with_name(&shader, configuration.filter)) {
   1591         init_shader_with_name(&shader, "NearestNeighbor");
   1592     }
   1593 
   1594 }
   1595 static const char *current_filter_name(unsigned index)
   1596 {
   1597     if (!uses_gl()) return "Requires OpenGL 3.2+";
   1598     unsigned i = 0;
   1599     for (; i < sizeof(shaders) / sizeof(shaders[0]); i++) {
   1600         if (strcmp(shaders[i].file_name, configuration.filter) == 0) {
   1601             break;
   1602         }
   1603     }
   1604     
   1605     if (i == sizeof(shaders) / sizeof(shaders[0])) {
   1606         i = 0;
   1607     }
   1608     
   1609     return shaders[i].display_name;
   1610 }
   1611 
   1612 static void cycle_blending_mode(unsigned index)
   1613 {
   1614         if (!uses_gl()) return;
   1615     if (configuration.blending_mode == GB_FRAME_BLENDING_MODE_ACCURATE) {
   1616         configuration.blending_mode = GB_FRAME_BLENDING_MODE_DISABLED;
   1617     }
   1618     else {
   1619         configuration.blending_mode++;
   1620     }
   1621 }
   1622 
   1623 static void cycle_blending_mode_backwards(unsigned index)
   1624 {
   1625     if (!uses_gl()) return;
   1626     if (configuration.blending_mode == GB_FRAME_BLENDING_MODE_DISABLED) {
   1627         configuration.blending_mode = GB_FRAME_BLENDING_MODE_ACCURATE;
   1628     }
   1629     else {
   1630         configuration.blending_mode--;
   1631     }
   1632 }
   1633 
   1634 static const char *blending_mode_string(unsigned index)
   1635 {
   1636     if (!uses_gl()) return "Requires OpenGL 3.2+";
   1637     return GB_inline_const(const char *[], {"Disabled", "Simple", "Accurate"})
   1638         [configuration.blending_mode];
   1639 }
   1640 
   1641 static void toggle_osd(unsigned index)
   1642 {
   1643     osd_countdown = 0;
   1644     configuration.osd = !configuration.osd;
   1645 }
   1646 
   1647 static const char *current_osd_mode(unsigned index)
   1648 {
   1649     return configuration.osd? "Enabled" : "Disabled";
   1650 }
   1651 
   1652 static const char *current_vsync_mode(unsigned index)
   1653 {
   1654     switch (configuration.vsync_mode) {
   1655         default:
   1656         case 0: return "Disabled";
   1657         case 1: return "Enabled";
   1658         case -1: return "Adaptive";
   1659     }
   1660 }
   1661 
   1662 static void cycle_vsync(unsigned index)
   1663 {
   1664 retry:
   1665     configuration.vsync_mode++;
   1666     if (configuration.vsync_mode == 2) {
   1667         configuration.vsync_mode = -1;
   1668     }
   1669     if (SDL_GL_SetSwapInterval(configuration.vsync_mode) && configuration.vsync_mode != 0) {
   1670         goto retry;
   1671     }
   1672 }
   1673 
   1674 static void cycle_vsync_backwards(unsigned index)
   1675 {
   1676 retry:
   1677     configuration.vsync_mode--;
   1678     if (configuration.vsync_mode == -2) {
   1679         configuration.vsync_mode = 1;
   1680     }
   1681     if (SDL_GL_SetSwapInterval(configuration.vsync_mode) && configuration.vsync_mode != 0) {
   1682         goto retry;
   1683     }
   1684 }
   1685 
   1686 #ifdef _WIN32
   1687 
   1688 // Don't use the standard header definitions because we might not have the newest headers
   1689 typedef enum {
   1690     DWM_CORNER_DEFAULT = 0,
   1691     DWM_CORNER_SQUARE = 1,
   1692     DWM_CORNER_ROUND = 2,
   1693     DWM_CORNER_ROUNDSMALL = 3
   1694 } DMW_corner_settings_t;
   1695 
   1696 #define DWM_CORNER_PREFERENCE 33
   1697 
   1698 void configure_window_corners(void)
   1699 {
   1700     SDL_SysWMinfo wmInfo;
   1701     SDL_VERSION(&wmInfo.version);
   1702     SDL_GetWindowWMInfo(window, &wmInfo);
   1703     HWND hwnd = wmInfo.info.win.window;
   1704     DMW_corner_settings_t pref = configuration.disable_rounded_corners? DWM_CORNER_SQUARE : DWM_CORNER_DEFAULT;
   1705     DwmSetWindowAttribute(hwnd, DWM_CORNER_PREFERENCE, &pref, sizeof(pref));
   1706 }
   1707 
   1708 static void toggle_corners(unsigned index)
   1709 {
   1710     configuration.disable_rounded_corners = !configuration.disable_rounded_corners;
   1711     configure_window_corners();
   1712 }
   1713 
   1714 static const char *current_corner_mode(unsigned index)
   1715 {
   1716     SDL_SysWMinfo wmInfo;
   1717     SDL_VERSION(&wmInfo.version);
   1718     SDL_GetWindowWMInfo(window, &wmInfo);
   1719     HWND hwnd = wmInfo.info.win.window;
   1720     DMW_corner_settings_t pref;
   1721     
   1722     if (DwmGetWindowAttribute(hwnd, DWM_CORNER_PREFERENCE, &pref, sizeof(pref)) ||
   1723         pref == DWM_CORNER_SQUARE) {
   1724         return "Square";
   1725     }
   1726     return "Rounded";
   1727 }
   1728 #endif
   1729 
   1730 static const struct menu_item graphics_menu[] = {
   1731     {"Scaling Mode:", cycle_scaling, current_scaling_mode, cycle_scaling_backwards},
   1732     {"Default Window Scale:", cycle_default_scale, current_default_scale, cycle_default_scale_backwards},
   1733     {"Scaling Filter:", cycle_filter, current_filter_name, cycle_filter_backwards},
   1734     {"Color Correction:", cycle_color_correction, current_color_correction_mode, cycle_color_correction_backwards},
   1735     {"Ambient Light Temp.:", decrease_color_temperature, current_color_temperature, increase_color_temperature},
   1736     {"Frame Blending:", cycle_blending_mode, blending_mode_string, cycle_blending_mode_backwards},
   1737     {"Mono Palette:", cycle_palette, current_palette, cycle_palette_backwards},
   1738     {"Display Border:", cycle_border_mode, current_border_mode, cycle_border_mode_backwards},
   1739     {"On-Screen Display:", toggle_osd, current_osd_mode, toggle_osd},
   1740     {"Vsync Mode:", cycle_vsync, current_vsync_mode, cycle_vsync_backwards},
   1741 #ifdef _WIN32
   1742     {"Window Corners:", toggle_corners, current_corner_mode, toggle_corners},
   1743 #endif
   1744     {"Back", enter_options_menu},
   1745     {NULL,}
   1746 };
   1747 
   1748 static void enter_graphics_menu(unsigned index)
   1749 {
   1750     current_menu = graphics_menu;
   1751     current_selection = 0;
   1752     scroll = 0;
   1753     recalculate_menu_height();
   1754 }
   1755 
   1756 static const char *highpass_filter_string(unsigned index)
   1757 {
   1758     return GB_inline_const(const char *[], {"None (Keep DC Offset)", "Accurate", "Preserve Waveform"})
   1759         [configuration.highpass_mode];
   1760 }
   1761 
   1762 static void cycle_highpass_filter(unsigned index)
   1763 {
   1764     configuration.highpass_mode++;
   1765     if (configuration.highpass_mode == GB_HIGHPASS_MAX) {
   1766         configuration.highpass_mode = 0;
   1767     }
   1768 }
   1769 
   1770 static void cycle_highpass_filter_backwards(unsigned index)
   1771 {
   1772     if (configuration.highpass_mode == 0) {
   1773         configuration.highpass_mode = GB_HIGHPASS_MAX - 1;
   1774     }
   1775     else {
   1776         configuration.highpass_mode--;
   1777     }
   1778 }
   1779 
   1780 static const char *volume_string(unsigned index)
   1781 {
   1782     static char ret[5];
   1783     sprintf(ret, "%d%%", configuration.volume);
   1784     return ret;
   1785 }
   1786 
   1787 static void increase_volume(unsigned index)
   1788 {
   1789     configuration.volume += 5;
   1790     if (configuration.volume > 100) {
   1791         configuration.volume = 100;
   1792     }
   1793 }
   1794 
   1795 static void decrease_volume(unsigned index)
   1796 {
   1797     configuration.volume -= 5;
   1798     if (configuration.volume > 100) {
   1799         configuration.volume = 0;
   1800     }
   1801 }
   1802 
   1803 static const char *interference_volume_string(unsigned index)
   1804 {
   1805     static char ret[5];
   1806     sprintf(ret, "%d%%", configuration.interference_volume);
   1807     return ret;
   1808 }
   1809 
   1810 static void increase_interference_volume(unsigned index)
   1811 {
   1812     configuration.interference_volume += 5;
   1813     if (configuration.interference_volume > 100) {
   1814         configuration.interference_volume = 100;
   1815     }
   1816 }
   1817 
   1818 static void decrease_interference_volume(unsigned index)
   1819 {
   1820     configuration.interference_volume -= 5;
   1821     if (configuration.interference_volume > 100) {
   1822         configuration.interference_volume = 0;
   1823     }
   1824 }
   1825 
   1826 static const char *audio_driver_string(unsigned index)
   1827 {
   1828     return GB_audio_driver_name();
   1829 }
   1830 
   1831 static const char *preferred_audio_driver_string(unsigned index)
   1832 {
   1833     if (configuration.audio_driver[0] == 0) {
   1834         return "Auto";
   1835     }
   1836     return configuration.audio_driver;
   1837 }
   1838 
   1839 static void audio_driver_changed(void);
   1840 
   1841 static void cycle_prefrered_audio_driver(unsigned index)
   1842 {
   1843     audio_driver_changed();
   1844     if (configuration.audio_driver[0] == 0) {
   1845         strcpy(configuration.audio_driver, GB_audio_driver_name_at_index(0));
   1846         return;
   1847     }
   1848     unsigned i = 0;
   1849     while (true) {
   1850         const char *name = GB_audio_driver_name_at_index(i);
   1851         if (name[0] == 0) { // Not a supported driver? Switch to auto
   1852             configuration.audio_driver[0] = 0;
   1853             return;
   1854         }
   1855         if (strcmp(configuration.audio_driver, name) == 0) {
   1856             strcpy(configuration.audio_driver, GB_audio_driver_name_at_index(i + 1));
   1857             return;
   1858         }
   1859         i++;
   1860     }
   1861 }
   1862 
   1863 static void cycle_preferred_audio_driver_backwards(unsigned index)
   1864 {
   1865     audio_driver_changed();
   1866     if (configuration.audio_driver[0] == 0) {
   1867         unsigned i = 0;
   1868         while (true) {
   1869             const char *name = GB_audio_driver_name_at_index(i);
   1870             if (name[0] == 0) {
   1871                 strcpy(configuration.audio_driver, GB_audio_driver_name_at_index(i - 1));
   1872                 return;
   1873             }
   1874             i++;
   1875         }
   1876         return;
   1877     }
   1878     unsigned i = 0;
   1879     while (true) {
   1880         const char *name = GB_audio_driver_name_at_index(i);
   1881         if (name[0] == 0) { // Not a supported driver? Switch to auto
   1882             configuration.audio_driver[0] = 0;
   1883             return;
   1884         }
   1885         if (strcmp(configuration.audio_driver, name) == 0) {
   1886             strcpy(configuration.audio_driver, GB_audio_driver_name_at_index(i - 1));
   1887             return;
   1888         }
   1889         i++;
   1890     }
   1891 }
   1892 
   1893 static struct menu_item audio_menu[] = {
   1894     {"Highpass Filter:", cycle_highpass_filter, highpass_filter_string, cycle_highpass_filter_backwards},
   1895     {"Volume:", increase_volume, volume_string, decrease_volume},
   1896     {"Interference Volume:", increase_interference_volume, interference_volume_string, decrease_interference_volume},
   1897     {"Preferred Audio Driver:", cycle_prefrered_audio_driver, preferred_audio_driver_string, cycle_preferred_audio_driver_backwards},
   1898     {"Active Driver:", nop, audio_driver_string},
   1899     {"Back", enter_options_menu},
   1900     {NULL,}
   1901 };
   1902 
   1903 static void audio_driver_changed(void)
   1904 {
   1905     audio_menu[4].value_getter = NULL;
   1906     audio_menu[4].string = "Relaunch to apply";
   1907 }
   1908 
   1909 static void enter_audio_menu(unsigned index)
   1910 {
   1911     current_menu = audio_menu;
   1912     current_selection = 0;
   1913     scroll = 0;
   1914     recalculate_menu_height();
   1915 }
   1916 
   1917 static void modify_key(unsigned index)
   1918 {
   1919     gui_state = WAITING_FOR_KEY;
   1920 }
   1921 
   1922 static const char *key_name(unsigned index);
   1923 
   1924 static const struct menu_item keyboard_menu[] = {
   1925     {"Right:", modify_key, key_name,},
   1926     {"Left:", modify_key, key_name,},
   1927     {"Up:", modify_key, key_name,},
   1928     {"Down:", modify_key, key_name,},
   1929     {"A:", modify_key, key_name,},
   1930     {"B:", modify_key, key_name,},
   1931     {"Select:", modify_key, key_name,},
   1932     {"Start:", modify_key, key_name,},
   1933     {"Turbo:", modify_key, key_name,},
   1934     {"Rewind:", modify_key, key_name,},
   1935     {"Slow-Motion:", modify_key, key_name,},
   1936     {"Rapid A:", modify_key, key_name,},
   1937     {"Rapid B:", modify_key, key_name,},
   1938     {"Back", enter_controls_menu},
   1939     {NULL,}
   1940 };
   1941 
   1942 static const char *key_name(unsigned index)
   1943 {
   1944     SDL_Scancode code = index >= GB_CONF_KEYS_COUNT? configuration.keys_2[index - GB_CONF_KEYS_COUNT] : configuration.keys[index];
   1945     if (!code) return "Not Set";
   1946     return SDL_GetScancodeName(code);
   1947 }
   1948 
   1949 static void enter_keyboard_menu(unsigned index)
   1950 {
   1951     current_menu = keyboard_menu;
   1952     current_selection = 0;
   1953     scroll = 0;
   1954     recalculate_menu_height();
   1955 }
   1956 
   1957 static unsigned joypad_index = 0;
   1958 static SDL_GameController *controller = NULL;
   1959 SDL_Haptic *haptic = NULL;
   1960 SDL_Joystick *joystick = NULL;
   1961 
   1962 static const char *current_joypad_name(unsigned index)
   1963 {
   1964     static char name[23] = {0,};
   1965     const char *orig_name = joystick? SDL_JoystickName(joystick) : NULL;
   1966     if (!orig_name) return "Not Found";
   1967     unsigned i = 0;
   1968     
   1969     // SDL returns a name with repeated and trailing spaces
   1970     while (*orig_name && i < sizeof(name) - 2) {
   1971         if (orig_name[0] != ' ' || orig_name[1] != ' ') {
   1972             name[i++] = *orig_name > 0? *orig_name : MOJIBAKE_STRING[0];
   1973         }
   1974         orig_name++;
   1975     }
   1976     if (i && name[i - 1] == ' ') {
   1977         i--;
   1978     }
   1979     name[i] = 0;
   1980     
   1981     return name;
   1982 }
   1983 
   1984 static void cycle_joypads(unsigned index)
   1985 {
   1986     joypad_index++;
   1987     if (joypad_index >= SDL_NumJoysticks()) {
   1988         joypad_index = 0;
   1989     }
   1990     
   1991     if (haptic) {
   1992         SDL_HapticClose(haptic);
   1993         haptic = NULL;
   1994     }
   1995     
   1996     if (controller) {
   1997         SDL_GameControllerClose(controller);
   1998         controller = NULL;
   1999     }
   2000     else if (joystick) {
   2001         SDL_JoystickClose(joystick);
   2002         joystick = NULL;
   2003     }
   2004     if ((controller = SDL_GameControllerOpen(joypad_index))) {
   2005         joystick = SDL_GameControllerGetJoystick(controller);
   2006     }
   2007     else {
   2008         joystick = SDL_JoystickOpen(joypad_index);
   2009     }
   2010     if (joystick) {
   2011         haptic = SDL_HapticOpenFromJoystick(joystick);
   2012     }
   2013 }
   2014 
   2015 static void cycle_joypads_backwards(unsigned index)
   2016 {
   2017     joypad_index--;
   2018     if (joypad_index >= SDL_NumJoysticks()) {
   2019         joypad_index = SDL_NumJoysticks() - 1;
   2020     }
   2021     
   2022     if (haptic) {
   2023         SDL_HapticClose(haptic);
   2024         haptic = NULL;
   2025     }
   2026     
   2027     if (controller) {
   2028         SDL_GameControllerClose(controller);
   2029         controller = NULL;
   2030     }
   2031     else if (joystick) {
   2032         SDL_JoystickClose(joystick);
   2033         joystick = NULL;
   2034     }
   2035     if ((controller = SDL_GameControllerOpen(joypad_index))) {
   2036         joystick = SDL_GameControllerGetJoystick(controller);
   2037     }
   2038     else {
   2039         joystick = SDL_JoystickOpen(joypad_index);
   2040     }
   2041     if (joystick) {
   2042         haptic = SDL_HapticOpenFromJoystick(joystick);
   2043     }}
   2044 
   2045 static void detect_joypad_layout(unsigned index)
   2046 {
   2047     gui_state = WAITING_FOR_JBUTTON;
   2048     joypad_configuration_progress = 0;
   2049     joypad_axis_temp = -1;
   2050 }
   2051 
   2052 static void cycle_rumble_mode(unsigned index)
   2053 {
   2054     if (configuration.rumble_mode == GB_RUMBLE_ALL_GAMES) {
   2055         configuration.rumble_mode = GB_RUMBLE_DISABLED;
   2056     }
   2057     else {
   2058         configuration.rumble_mode++;
   2059     }
   2060 }
   2061 
   2062 static void cycle_rumble_mode_backwards(unsigned index)
   2063 {
   2064     if (configuration.rumble_mode == GB_RUMBLE_DISABLED) {
   2065         configuration.rumble_mode = GB_RUMBLE_ALL_GAMES;
   2066     }
   2067     else {
   2068         configuration.rumble_mode--;
   2069     }
   2070 }
   2071 
   2072 static const char *current_rumble_mode(unsigned index)
   2073 {
   2074     return GB_inline_const(const char *[], {"Disabled", "Rumble Game Paks Only", "All Games"})
   2075         [configuration.rumble_mode];
   2076 }
   2077 
   2078 static void toggle_use_faux_analog_inputs(unsigned index)
   2079 {
   2080     configuration.use_faux_analog_inputs ^= true;
   2081 }
   2082 
   2083 static const char *current_faux_analog_inputs(unsigned index)
   2084 {
   2085     return configuration.use_faux_analog_inputs? "Faux Analog" : "Digital";
   2086 }
   2087 
   2088 static void toggle_allow_background_controllers(unsigned index)
   2089 {
   2090     configuration.allow_background_controllers ^= true;
   2091     
   2092     SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS,
   2093                 configuration.allow_background_controllers? "1" : "0");
   2094 }
   2095 
   2096 static const char *current_background_control_mode(unsigned index)
   2097 {
   2098     return configuration.allow_background_controllers? "Always" : "During Window Focus Only";
   2099 }
   2100 
   2101 static void cycle_hotkey(unsigned index)
   2102 {
   2103     if (configuration.hotkey_actions[index - 2] == HOTKEY_MAX) {
   2104         configuration.hotkey_actions[index - 2] = 0;
   2105     }
   2106     else {
   2107         configuration.hotkey_actions[index - 2]++;
   2108     }
   2109 }
   2110 
   2111 static void cycle_hotkey_backwards(unsigned index)
   2112 {
   2113     if (configuration.hotkey_actions[index - 2] == 0) {
   2114         configuration.hotkey_actions[index - 2] = HOTKEY_MAX;
   2115     }
   2116     else {
   2117         configuration.hotkey_actions[index - 2]--;
   2118     }
   2119 }
   2120 
   2121 static const char *current_hotkey(unsigned index)
   2122 {
   2123     return GB_inline_const(const char *[], {
   2124         "None",
   2125         "Toggle Pause",
   2126         "Toggle Mute",
   2127         "Reset", 
   2128         "Quit SameBoy",
   2129         "Save State Slot 1",
   2130         "Load State Slot 1",
   2131         "Save State Slot 2",
   2132         "Load State Slot 2",
   2133         "Save State Slot 3",
   2134         "Load State Slot 3",
   2135         "Save State Slot 4",
   2136         "Load State Slot 4",
   2137         "Save State Slot 5",
   2138         "Load State Slot 5",
   2139         "Save State Slot 6",
   2140         "Load State Slot 6",
   2141         "Save State Slot 7",
   2142         "Load State Slot 7",
   2143         "Save State Slot 8",
   2144         "Load State Slot 8",
   2145         "Save State Slot 9",
   2146         "Load State Slot 9",
   2147         "Save State Slot 10",
   2148         "Load State Slot 10",
   2149     }) [configuration.hotkey_actions[index - 2]];
   2150 }
   2151 
   2152 static void increase_rumble_strength(unsigned index)
   2153 {
   2154     if (configuration.rumble_strength < 8) {
   2155         configuration.rumble_strength++;
   2156     }
   2157 }
   2158 
   2159 static void decrease_rumble_strength(unsigned index)
   2160 {
   2161     if (configuration.rumble_strength > 1) {
   2162         configuration.rumble_strength--;
   2163     }
   2164 }
   2165 
   2166 const char *current_rumble_strength(unsigned index)
   2167 {
   2168     static char ret[22];
   2169     strcpy(ret, TICKLESS_SLIDER_STRING);
   2170     unsigned pos = ((configuration.rumble_strength - 1) * (strlen(TICKLESS_SLIDER_STRING) - 1) + 3) / 7;
   2171     ret[pos] = SELECTED_SLIDER_STRING[pos];
   2172     return ret;
   2173 }
   2174 
   2175 static const struct menu_item joypad_menu[] = {
   2176     {"Joypad:", cycle_joypads, current_joypad_name, cycle_joypads_backwards},
   2177     {"Configure layout", detect_joypad_layout},
   2178     {"Hotkey 1 Action:", cycle_hotkey, current_hotkey, cycle_hotkey_backwards},
   2179     {"Hotkey 2 Action:", cycle_hotkey, current_hotkey, cycle_hotkey_backwards},
   2180     {"Rumble Mode:", cycle_rumble_mode, current_rumble_mode, cycle_rumble_mode_backwards},
   2181     {"Rumble Strength:", increase_rumble_strength, current_rumble_strength, decrease_rumble_strength},
   2182     {"Analog Stick Behavior:", toggle_use_faux_analog_inputs, current_faux_analog_inputs, toggle_use_faux_analog_inputs},
   2183     {"Enable Control:", toggle_allow_background_controllers, current_background_control_mode, toggle_allow_background_controllers},
   2184     {"Back", enter_controls_menu},
   2185     {NULL,}
   2186 };
   2187 
   2188 static void enter_joypad_menu(unsigned index)
   2189 {
   2190     current_menu = joypad_menu;
   2191     current_selection = 0;
   2192     scroll = 0;
   2193     recalculate_menu_height();
   2194 }
   2195 
   2196 joypad_button_t get_joypad_button(uint8_t physical_button)
   2197 {
   2198     for (unsigned i = 0; i < JOYPAD_BUTTONS_MAX; i++) {
   2199         if (configuration.joypad_configuration[i] == physical_button) {
   2200             return i;
   2201         }
   2202     }
   2203     return JOYPAD_BUTTONS_MAX;
   2204 }
   2205 
   2206 joypad_axis_t get_joypad_axis(uint8_t physical_axis)
   2207 {
   2208     for (unsigned i = 0; i < JOYPAD_AXISES_MAX; i++) {
   2209         if (configuration.joypad_axises[i] == physical_axis) {
   2210             return i;
   2211         }
   2212     }
   2213     return JOYPAD_AXISES_MAX;
   2214 }
   2215 
   2216 
   2217 void connect_joypad(void)
   2218 {
   2219     if (joystick && !SDL_NumJoysticks()) {
   2220         if (controller) {
   2221             SDL_GameControllerClose(controller);
   2222             controller = NULL;
   2223             joystick = NULL;
   2224         }
   2225         else {
   2226             SDL_JoystickClose(joystick);
   2227             joystick = NULL;
   2228         }
   2229     }
   2230     else if (!joystick && SDL_NumJoysticks()) {
   2231         if ((controller = SDL_GameControllerOpen(0))) {
   2232             joystick = SDL_GameControllerGetJoystick(controller);
   2233         }
   2234         else {
   2235             joystick = SDL_JoystickOpen(0);
   2236         }
   2237     }
   2238     if (joystick) {
   2239         haptic = SDL_HapticOpenFromJoystick(joystick);
   2240     }
   2241 }
   2242 
   2243 static void toggle_mouse_control(unsigned index)
   2244 {
   2245     configuration.allow_mouse_controls = !configuration.allow_mouse_controls;
   2246 }
   2247 
   2248 static const char *mouse_control_string(unsigned index)
   2249 {
   2250     return configuration.allow_mouse_controls? "Allow mouse control" : "Disallow mouse control";
   2251 }
   2252 
   2253 static const struct menu_item controls_menu[] = {
   2254     {"Keyboard Options", enter_keyboard_menu},
   2255     {"Joypad Options", enter_joypad_menu},
   2256     {"Motion-controlled games:", toggle_mouse_control, mouse_control_string, toggle_mouse_control},
   2257     {"Back", enter_options_menu},
   2258     {NULL,}
   2259 };
   2260 
   2261 static void enter_controls_menu(unsigned index)
   2262 {
   2263     current_menu = controls_menu;
   2264     current_selection = 0;
   2265     scroll = 0;
   2266     recalculate_menu_height();
   2267 }
   2268 
   2269 static void toggle_audio_recording(unsigned index)
   2270 {
   2271     if (!GB_is_inited(&gb)) {
   2272         SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", "Cannot start audio recording, open a ROM file first.", window);
   2273         return;
   2274     }
   2275     static bool is_recording = false;
   2276     if (is_recording) {
   2277         is_recording = false;
   2278         show_osd_text("Audio recording ended");
   2279         int error = GB_stop_audio_recording(&gb);
   2280         if (error) {
   2281             char *message = NULL;
   2282             asprintf(&message, "Could not finalize recording: %s", strerror(error));
   2283             SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", message, window);
   2284             free(message);
   2285         }
   2286         static const char item_string[] = "Start Audio Recording";
   2287         memcpy(audio_recording_menu_item, item_string, sizeof(item_string));
   2288         return;
   2289     }
   2290     char *filename = do_save_recording_dialog(GB_get_sample_rate(&gb));
   2291     
   2292     /* Drop events as it SDL seems to catch several in-dialog events */
   2293     SDL_Event event;
   2294     while (SDL_PollEvent(&event));
   2295     
   2296     if (filename) {
   2297         GB_audio_format_t format = GB_AUDIO_FORMAT_RAW;
   2298         size_t length = strlen(filename);
   2299         if (length >= 5) {
   2300             if (strcasecmp(".aiff", filename + length - 5) == 0) {
   2301                 format = GB_AUDIO_FORMAT_AIFF;
   2302             }
   2303             else if (strcasecmp(".aifc", filename + length - 5) == 0) {
   2304                 format = GB_AUDIO_FORMAT_AIFF;
   2305             }
   2306             else if (length >= 4) {
   2307                 if (strcasecmp(".aif", filename + length - 4) == 0) {
   2308                     format = GB_AUDIO_FORMAT_AIFF;
   2309                 }
   2310                 else if (strcasecmp(".wav", filename + length - 4) == 0) {
   2311                     format = GB_AUDIO_FORMAT_WAV;
   2312                 }
   2313             }
   2314         }
   2315         
   2316         int error = GB_start_audio_recording(&gb, filename, format);
   2317         free(filename);
   2318         if (error) {
   2319             char *message = NULL;
   2320             asprintf(&message, "Could not finalize recording: %s", strerror(error));
   2321             SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", message, window);
   2322             free(message);
   2323             return;
   2324         }
   2325         
   2326         is_recording = true;
   2327         static const char item_string[] = "Stop Audio Recording";
   2328         memcpy(audio_recording_menu_item, item_string, sizeof(item_string));
   2329         show_osd_text("Audio recording started");
   2330     }
   2331 }
   2332 
   2333 void convert_mouse_coordinates(signed *x, signed *y)
   2334 {
   2335     signed width = GB_get_screen_width(&gb);
   2336     signed height = GB_get_screen_height(&gb);
   2337     signed x_offset = (width - 160) / 2;
   2338     signed y_offset = (height - 144) / 2;
   2339 
   2340     *x = (signed)(*x - rect.x / factor) * width / (signed)(rect.w / factor) - x_offset;
   2341     *y = (signed)(*y - rect.y / factor) * height / (signed)(rect.h / factor) - y_offset;
   2342 
   2343     if (strcmp("CRT", configuration.filter) == 0) {
   2344         *y = *y * 8 / 7;
   2345         *y -= 144 / 16;
   2346     }
   2347 }
   2348 
   2349 void update_swap_interval(void)
   2350 {
   2351     SDL_DisplayMode mode;
   2352     SDL_GetCurrentDisplayMode(SDL_GetWindowDisplayIndex(window), &mode);
   2353     if (mode.refresh_rate >= 60) {
   2354         if (SDL_GL_SetSwapInterval(1)) {
   2355             SDL_GL_SetSwapInterval(0);
   2356         }
   2357     }
   2358     else {
   2359         SDL_GL_SetSwapInterval(0);
   2360     }
   2361 }
   2362 
   2363 void run_gui(bool is_running)
   2364 {
   2365     SDL_ShowCursor(SDL_ENABLE);
   2366     connect_joypad();
   2367     
   2368     /* Draw the background screen */
   2369     if (!converted_background) {
   2370         if (configuration.gui_palette_enabled) {
   2371             update_gui_palette();
   2372         }
   2373         else {
   2374             SDL_Surface *background = SDL_LoadBMP(resource_path("background.bmp"));
   2375             
   2376             /* Create a blank background if background.bmp could not be loaded */
   2377             if (!background) {
   2378                 background = SDL_CreateRGBSurface(0, 160, 144, 8, 0, 0, 0, 0);
   2379             }
   2380             SDL_SetPaletteColors(background->format->palette, gui_palette, 0, 4);
   2381             converted_background = SDL_ConvertSurface(background, pixel_format, 0);
   2382             SDL_FreeSurface(background);
   2383     
   2384             for (unsigned i = 4; i--; ) {
   2385                 gui_palette_native[i] = SDL_MapRGB(pixel_format, gui_palette[i].r, gui_palette[i].g, gui_palette[i].b);
   2386             }
   2387         }
   2388     }
   2389 
   2390     unsigned width = GB_get_screen_width(&gb);
   2391     unsigned height = GB_get_screen_height(&gb);
   2392     unsigned x_offset = (width - 160) / 2;
   2393     unsigned y_offset = (height - 144) / 2;
   2394     uint32_t pixels[width * height];
   2395     
   2396     if (width != 160 || height != 144) {
   2397         for (unsigned i = 0; i < width * height; i++) {
   2398             pixels[i] = gui_palette_native[0];
   2399         }
   2400     }
   2401     
   2402     SDL_Event event = {0,};
   2403     gui_state = is_running? SHOWING_MENU : SHOWING_DROP_MESSAGE;
   2404     bool should_render = true;
   2405     current_menu = root_menu = is_running? paused_menu : nonpaused_menu;
   2406     recalculate_menu_height();
   2407     current_selection = 0;
   2408     scroll = 0;
   2409     
   2410     bool scrollbar_drag = false;
   2411     signed scroll_mouse_start = 0;
   2412     signed scroll_start = 0;
   2413     while (true) {
   2414         SDL_WaitEvent(&event);
   2415         /* Convert Joypad and mouse events (We only generate down events) */
   2416         if (gui_state != WAITING_FOR_KEY && gui_state != WAITING_FOR_JBUTTON && gui_state != TEXT_INPUT) {
   2417             switch (event.type) {
   2418                 case SDL_KEYDOWN:
   2419                     if (gui_state == WAITING_FOR_KEY) break;
   2420                     if (event.key.keysym.mod != 0) break;
   2421                     switch (event.key.keysym.scancode) {
   2422                         // Do not remap these keys to prevent deadlocking
   2423                         case SDL_SCANCODE_ESCAPE:
   2424                         case SDL_SCANCODE_RETURN:
   2425                         case SDL_SCANCODE_RIGHT:
   2426                         case SDL_SCANCODE_LEFT:
   2427                         case SDL_SCANCODE_UP:
   2428                         case SDL_SCANCODE_DOWN:
   2429                         case SDL_SCANCODE_H:
   2430                         case SDL_SCANCODE_J:
   2431                         case SDL_SCANCODE_K:
   2432                         case SDL_SCANCODE_L:
   2433                             break;
   2434                             
   2435                         default:
   2436                                  if (event.key.keysym.scancode == configuration.keys[GB_KEY_RIGHT]) event.key.keysym.scancode = SDL_SCANCODE_RIGHT;
   2437                             else if (event.key.keysym.scancode == configuration.keys[GB_KEY_LEFT]) event.key.keysym.scancode = SDL_SCANCODE_LEFT;
   2438                             else if (event.key.keysym.scancode == configuration.keys[GB_KEY_UP]) event.key.keysym.scancode = SDL_SCANCODE_UP;
   2439                             else if (event.key.keysym.scancode == configuration.keys[GB_KEY_DOWN]) event.key.keysym.scancode = SDL_SCANCODE_DOWN;
   2440                             else if (event.key.keysym.scancode == configuration.keys[GB_KEY_A]) event.key.keysym.scancode = SDL_SCANCODE_RETURN;
   2441                             else if (event.key.keysym.scancode == configuration.keys[GB_KEY_START]) event.key.keysym.scancode = SDL_SCANCODE_RETURN;
   2442                             else if (event.key.keysym.scancode == configuration.keys[GB_KEY_B]) event.key.keysym.scancode = SDL_SCANCODE_ESCAPE;
   2443                             break;
   2444                     }
   2445                     break;
   2446 
   2447                 case SDL_WINDOWEVENT:
   2448                     should_render = true;
   2449                     break;
   2450                 case SDL_MOUSEBUTTONUP:
   2451                     scrollbar_drag = false;
   2452                     break;
   2453                 case SDL_MOUSEBUTTONDOWN:
   2454                     if (gui_state == SHOWING_HELP) {
   2455                         event.type = SDL_KEYDOWN;
   2456                         event.key.keysym.scancode = SDL_SCANCODE_RETURN;
   2457                     }
   2458                     else if (gui_state == SHOWING_DROP_MESSAGE) {
   2459                         event.type = SDL_KEYDOWN;
   2460                         event.key.keysym.scancode = SDL_SCANCODE_ESCAPE;
   2461                     }
   2462                     else if (gui_state == SHOWING_MENU) {
   2463                         signed x = event.button.x;
   2464                         signed y = event.button.y;
   2465                         convert_mouse_coordinates(&x, &y);
   2466                         if (x >= 160 - 6 && x < 160 && menu_height > 144) {
   2467                             unsigned scrollbar_offset = (140 - scrollbar_size) * scroll / (menu_height - 144);
   2468                             if (scrollbar_offset + scrollbar_size > 140) {
   2469                                 scrollbar_offset = 140 - scrollbar_size;
   2470                             }
   2471                             
   2472                             if (y < scrollbar_offset || y > scrollbar_offset + scrollbar_size) {
   2473                                 scroll = (menu_height - 144) * y / 143;
   2474                                 should_render = true;
   2475                             }
   2476 
   2477                             scrollbar_drag = true;
   2478                             mouse_scroling = true;
   2479                             scroll_mouse_start = y;
   2480                             scroll_start = scroll;
   2481                             break;
   2482                         }
   2483                         y += scroll;
   2484                         
   2485                         if (x < 0 || x >= 160 || y < 24) {
   2486                             continue;
   2487                         }
   2488                         
   2489                         unsigned item_y = 24;
   2490                         unsigned index = 0;
   2491                         for (const struct menu_item *item = current_menu; item->string; item++, index++) {
   2492                             if (!item->backwards_handler) {
   2493                                 if (y >= item_y && y < item_y + 12) {
   2494                                     break;
   2495                                 }
   2496                                 item_y += 12;
   2497                             }
   2498                             else {
   2499                                 if (y >= item_y && y < item_y + 24) {
   2500                                     break;
   2501                                 }
   2502                                 item_y += 24;
   2503                             }
   2504                         }
   2505                         
   2506                         if (!current_menu[index].string) continue;
   2507                         
   2508                         current_selection = index;
   2509                         event.type = SDL_KEYDOWN;
   2510                         if (current_menu[index].backwards_handler) {
   2511                             event.key.keysym.scancode = x < 80? SDL_SCANCODE_LEFT : SDL_SCANCODE_RIGHT;
   2512                         }
   2513                         else {
   2514                             event.key.keysym.scancode = SDL_SCANCODE_RETURN;
   2515                         }
   2516 
   2517                     }
   2518                     break;
   2519                 case SDL_JOYBUTTONDOWN:
   2520                     event.type = SDL_KEYDOWN;
   2521                     joypad_button_t button = get_joypad_button(event.jbutton.button);
   2522                     if (button == JOYPAD_BUTTON_A) {
   2523                         event.key.keysym.scancode = SDL_SCANCODE_RETURN;
   2524                     }
   2525                     else if (button == JOYPAD_BUTTON_MENU || button == JOYPAD_BUTTON_B) {
   2526                         event.key.keysym.scancode = SDL_SCANCODE_ESCAPE;
   2527                     }
   2528                     else if (button == JOYPAD_BUTTON_UP) event.key.keysym.scancode = SDL_SCANCODE_UP;
   2529                     else if (button == JOYPAD_BUTTON_DOWN) event.key.keysym.scancode = SDL_SCANCODE_DOWN;
   2530                     else if (button == JOYPAD_BUTTON_LEFT) event.key.keysym.scancode = SDL_SCANCODE_LEFT;
   2531                     else if (button == JOYPAD_BUTTON_RIGHT) event.key.keysym.scancode = SDL_SCANCODE_RIGHT;
   2532                     break;
   2533 
   2534                 case SDL_JOYHATMOTION: {
   2535                     uint8_t value = event.jhat.value;
   2536                     if (value != 0) {
   2537                         uint32_t scancode =
   2538                             value == SDL_HAT_UP ? SDL_SCANCODE_UP
   2539                             : value == SDL_HAT_DOWN ? SDL_SCANCODE_DOWN
   2540                             : value == SDL_HAT_LEFT ? SDL_SCANCODE_LEFT
   2541                             : value == SDL_HAT_RIGHT ? SDL_SCANCODE_RIGHT
   2542                             : 0;
   2543 
   2544                         if (scancode != 0) {
   2545                             event.type = SDL_KEYDOWN;
   2546                             event.key.keysym.scancode = scancode;
   2547                         }
   2548                     }
   2549                     break;
   2550                }
   2551                     
   2552                 case SDL_JOYAXISMOTION: {
   2553                     static bool axis_active[2] = {false, false};
   2554                     joypad_axis_t axis = get_joypad_axis(event.jaxis.axis);
   2555                     if (axis == JOYPAD_AXISES_X) {
   2556                         if (!axis_active[0] && event.jaxis.value > JOYSTICK_HIGH) {
   2557                             axis_active[0] = true;
   2558                             event.type = SDL_KEYDOWN;
   2559                             event.key.keysym.scancode = SDL_SCANCODE_RIGHT;
   2560                         }
   2561                         else if (!axis_active[0] && event.jaxis.value < -JOYSTICK_HIGH) {
   2562                             axis_active[0] = true;
   2563                             event.type = SDL_KEYDOWN;
   2564                             event.key.keysym.scancode = SDL_SCANCODE_LEFT;
   2565                             
   2566                         }
   2567                         else if (axis_active[0] && event.jaxis.value < JOYSTICK_LOW && event.jaxis.value > -JOYSTICK_LOW) {
   2568                             axis_active[0] = false;
   2569                         }
   2570                     }
   2571                     else if (axis == JOYPAD_AXISES_Y) {
   2572                         if (!axis_active[1] && event.jaxis.value > JOYSTICK_HIGH) {
   2573                             axis_active[1] = true;
   2574                             event.type = SDL_KEYDOWN;
   2575                             event.key.keysym.scancode = SDL_SCANCODE_DOWN;
   2576                         }
   2577                         else if (!axis_active[1] && event.jaxis.value < -JOYSTICK_HIGH) {
   2578                             axis_active[1] = true;
   2579                             event.type = SDL_KEYDOWN;
   2580                             event.key.keysym.scancode = SDL_SCANCODE_UP;
   2581                         }
   2582                         else if (axis_active[1] && event.jaxis.value < JOYSTICK_LOW && event.jaxis.value > -JOYSTICK_LOW) {
   2583                             axis_active[1] = false;
   2584                         }
   2585                     }
   2586                 }
   2587             }
   2588         }
   2589         switch (event.type) {
   2590             case SDL_DISPLAYEVENT:
   2591                 update_swap_interval();
   2592                 break;
   2593             case SDL_QUIT: {
   2594                 if (!is_running) {
   2595                     exit(0);
   2596                 }
   2597                 else {
   2598                     pending_command = GB_SDL_QUIT_COMMAND;
   2599                     return;
   2600                 }
   2601                 
   2602             }
   2603             case SDL_WINDOWEVENT: {
   2604                 if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
   2605                     update_viewport();
   2606                     render_texture(NULL, NULL);
   2607                     screen_manually_resized = true;
   2608                 }
   2609                 if (event.window.type == SDL_WINDOWEVENT_MOVED
   2610 #if SDL_COMPILEDVERSION > 2018
   2611                     || event.window.type == SDL_WINDOWEVENT_DISPLAY_CHANGED
   2612 #endif
   2613                     ) {
   2614                     update_swap_interval();
   2615                 }
   2616                 break;
   2617             }
   2618             case SDL_DROPFILE: {
   2619                 if (GB_is_save_state(event.drop.file)) {
   2620                     if (GB_is_inited(&gb)) {
   2621                         dropped_state_file = event.drop.file;
   2622                         pending_command = GB_SDL_LOAD_STATE_FROM_FILE_COMMAND;
   2623                     }
   2624                     else {
   2625                         SDL_free(event.drop.file);
   2626                     }
   2627                     break;
   2628                 }
   2629                 else {
   2630                     set_filename(event.drop.file, SDL_free);
   2631                     pending_command = GB_SDL_NEW_FILE_COMMAND;
   2632                     return;
   2633                 }
   2634             }
   2635             case SDL_JOYBUTTONDOWN: {
   2636                 if (gui_state == WAITING_FOR_JBUTTON && joypad_configuration_progress != JOYPAD_BUTTONS_MAX) {
   2637                     should_render = true;
   2638                     configuration.joypad_configuration[joypad_configuration_progress++] = event.jbutton.button;
   2639                 }
   2640                 break;
   2641             }
   2642             case SDL_JOYHATMOTION: {
   2643                 if (gui_state == WAITING_FOR_JBUTTON && joypad_configuration_progress == JOYPAD_BUTTON_RIGHT) {
   2644                     should_render = true;
   2645                     configuration.joypad_configuration[joypad_configuration_progress++] = -1;
   2646                     configuration.joypad_configuration[joypad_configuration_progress++] = -1;
   2647                     configuration.joypad_configuration[joypad_configuration_progress++] = -1;
   2648                     configuration.joypad_configuration[joypad_configuration_progress++] = -1;
   2649                 }
   2650                 break;
   2651             }
   2652                 
   2653             case SDL_JOYAXISMOTION: {
   2654                 if (gui_state == WAITING_FOR_JBUTTON &&
   2655                     joypad_configuration_progress == JOYPAD_BUTTONS_MAX &&
   2656                     abs(event.jaxis.value) >= 0x4000) {
   2657                     if (joypad_axis_temp == (uint8_t)-1) {
   2658                         joypad_axis_temp = event.jaxis.axis;
   2659                     }
   2660                     else if (joypad_axis_temp != event.jaxis.axis) {
   2661                         if (joypad_axis_temp < event.jaxis.axis) {
   2662                             configuration.joypad_axises[JOYPAD_AXISES_X] = joypad_axis_temp;
   2663                             configuration.joypad_axises[JOYPAD_AXISES_Y] = event.jaxis.axis;
   2664                         }
   2665                         else {
   2666                             configuration.joypad_axises[JOYPAD_AXISES_Y] = joypad_axis_temp;
   2667                             configuration.joypad_axises[JOYPAD_AXISES_X] = event.jaxis.axis;
   2668                         }
   2669                         
   2670                         gui_state = SHOWING_MENU;
   2671                         should_render = true;
   2672                     }
   2673                 }
   2674                 break;
   2675             }
   2676                 
   2677             case SDL_MOUSEWHEEL: {
   2678                 if (menu_height > 144) {
   2679                     scroll -= event.wheel.y;
   2680                     if (scroll < 0) {
   2681                         scroll = 0;
   2682                     }
   2683                     if (scroll >= menu_height - 144) {
   2684                         scroll = menu_height - 144;
   2685                     }
   2686 
   2687                     mouse_scroling = true;
   2688                     should_render = true;
   2689                 }
   2690                 break;
   2691             }
   2692                 
   2693             case SDL_MOUSEMOTION: {
   2694                 if (scrollbar_drag && scrollbar_size < 140 && scrollbar_size > 0) {
   2695                     signed x = event.motion.x;
   2696                     signed y = event.motion.y;
   2697                     convert_mouse_coordinates(&x, &y);
   2698                     signed delta = scroll_mouse_start - y;
   2699                     scroll = scroll_start - delta * (signed)(menu_height - 144) / (signed)(140 - scrollbar_size);
   2700                     if (scroll < 0) {
   2701                         scroll = 0;
   2702                     }
   2703                     if (scroll >= menu_height - 144) {
   2704                         scroll = menu_height - 144;
   2705                     }
   2706 
   2707                     should_render = true;
   2708                 }
   2709                 break;
   2710             }
   2711                 
   2712 
   2713             case SDL_TEXTINPUT:
   2714                 if (gui_state == TEXT_INPUT) {
   2715                     char *s = event.text.text;
   2716                     while (*s) {
   2717                         text_input_callback(*(s++));
   2718                     }
   2719                     should_render = true;
   2720                 }
   2721                 break;
   2722             case SDL_KEYDOWN:
   2723                 scrollbar_drag = false;
   2724                 if (gui_state == TEXT_INPUT) {
   2725                     if (event.key.keysym.sym == SDLK_v && (event.key.keysym.mod & MODIFIER)) {
   2726                         char *s = SDL_GetClipboardText();
   2727                         while (*s) {
   2728                             text_input_callback(*(s++));
   2729                         }
   2730                         should_render = true;
   2731                     }
   2732                     else if (event.key.keysym.scancode == SDL_SCANCODE_BACKSPACE) {
   2733                         text_input_callback('\b');
   2734                         should_render = true;
   2735                     }
   2736                     else if (event.key.keysym.scancode == SDL_SCANCODE_RETURN ||
   2737                              event.key.keysym.scancode == SDL_SCANCODE_RETURN2 ||
   2738                              event.key.keysym.scancode == SDL_SCANCODE_KP_ENTER) {
   2739                         text_input_callback('\n');
   2740                         should_render = true;
   2741                     }
   2742                 }
   2743                 else if (gui_state == WAITING_FOR_KEY) {
   2744                     if (current_selection > 8) {
   2745                         configuration.keys_2[current_selection - GB_CONF_KEYS_COUNT] = event.key.keysym.scancode;
   2746                     }
   2747                     else {
   2748                         configuration.keys[current_selection] = event.key.keysym.scancode;
   2749                     }
   2750                     gui_state = SHOWING_MENU;
   2751                     should_render = true;
   2752                 }
   2753                 else if (event_hotkey_code(&event) == SDL_SCANCODE_F && event.key.keysym.mod & MODIFIER) {
   2754                     if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == false) {
   2755                         SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP);
   2756                     }
   2757                     else {
   2758                         SDL_SetWindowFullscreen(window, 0);
   2759                     }
   2760                     update_swap_interval();
   2761                     update_viewport();
   2762                     screen_manually_resized = true;
   2763                 }
   2764                 else if (event_hotkey_code(&event) == SDL_SCANCODE_O) {
   2765                     if (event.key.keysym.mod & MODIFIER) {
   2766                         char *filename = do_open_rom_dialog();
   2767                         if (filename) {
   2768                             set_filename(filename, free);
   2769                             pending_command = GB_SDL_NEW_FILE_COMMAND;
   2770                             return;
   2771                         }
   2772                     }
   2773                 }
   2774                 else if (event.key.keysym.scancode == SDL_SCANCODE_RETURN && gui_state == WAITING_FOR_JBUTTON) {
   2775                     should_render = true;
   2776                     if (joypad_configuration_progress != JOYPAD_BUTTONS_MAX) {
   2777                         configuration.joypad_configuration[joypad_configuration_progress] = -1;
   2778                     }
   2779                     else {
   2780                         configuration.joypad_axises[0] = -1;
   2781                         configuration.joypad_axises[1] = -1;
   2782                     }
   2783                     joypad_configuration_progress++;
   2784                     
   2785                     if (joypad_configuration_progress > JOYPAD_BUTTONS_MAX) {
   2786                         gui_state = SHOWING_MENU;
   2787                     }
   2788                 }
   2789                 else if (event.key.keysym.scancode == SDL_SCANCODE_ESCAPE) {
   2790                     if (gui_state == SHOWING_MENU && current_menu != root_menu) {
   2791                         for (const struct menu_item *item = current_menu; item->string; item++) {
   2792                             if (strcmp(item->string, "Back") == 0) {
   2793                                 item->handler(0);
   2794                                 goto handle_pending;
   2795                                 break;
   2796                             }
   2797                         }
   2798                         should_render = true;
   2799                     }
   2800                     else if (is_running) {
   2801                         SDL_StopTextInput();
   2802                         return;
   2803                     }
   2804                     else {
   2805                         if (gui_state == SHOWING_DROP_MESSAGE) {
   2806                             gui_state = SHOWING_MENU;
   2807                         }
   2808                         else if (gui_state == SHOWING_MENU) {
   2809                             gui_state = SHOWING_DROP_MESSAGE;
   2810                         }
   2811                         current_selection = 0;
   2812                         mouse_scroling = false;
   2813                         scroll = 0;
   2814                         current_menu = root_menu;
   2815                         recalculate_menu_height();
   2816                         should_render = true;
   2817                     }
   2818                 }
   2819                 else if (gui_state == SHOWING_MENU) {
   2820                     if ((event.key.keysym.scancode == SDL_SCANCODE_DOWN ||
   2821                          event.key.keysym.scancode == SDL_SCANCODE_J) &&
   2822                         current_menu[current_selection + 1].string) {
   2823                         current_selection++;
   2824                         mouse_scroling = false;
   2825                         should_render = true;
   2826                     }
   2827                     else if ((event.key.keysym.scancode == SDL_SCANCODE_UP ||
   2828                               event.key.keysym.scancode == SDL_SCANCODE_K) &&
   2829                              current_selection) {
   2830                         current_selection--;
   2831                         mouse_scroling = false;
   2832                         should_render = true;
   2833                     }
   2834                     else if (event.key.keysym.scancode == SDL_SCANCODE_RETURN  && !current_menu[current_selection].backwards_handler) {
   2835                         if (current_menu[current_selection].handler) {
   2836                             current_menu[current_selection].handler(current_selection);
   2837                             handle_pending:
   2838                             if (pending_command == GB_SDL_RESET_COMMAND && !is_running) {
   2839                                 pending_command = GB_SDL_NO_COMMAND;
   2840                             }
   2841                             if (pending_command) {
   2842                                 if (!is_running && pending_command == GB_SDL_QUIT_COMMAND) {
   2843                                     exit(0);
   2844                                 }
   2845                                 return;
   2846                             }
   2847                             should_render = true;
   2848                         }
   2849                         else {
   2850                             return;
   2851                         }
   2852                     }
   2853                     else if ((event.key.keysym.scancode == SDL_SCANCODE_RIGHT ||
   2854                               event.key.keysym.scancode == SDL_SCANCODE_L) &&
   2855                              current_menu[current_selection].backwards_handler) {
   2856                         current_menu[current_selection].handler(current_selection);
   2857                         should_render = true;
   2858                     }
   2859                     else if ((event.key.keysym.scancode == SDL_SCANCODE_LEFT ||
   2860                               event.key.keysym.scancode == SDL_SCANCODE_H) &&
   2861                              current_menu[current_selection].backwards_handler) {
   2862                         current_menu[current_selection].backwards_handler(current_selection);
   2863                         should_render = true;
   2864                     }
   2865                 }
   2866                 else if (gui_state == SHOWING_HELP) {
   2867                     gui_state = SHOWING_MENU;
   2868                     should_render = true;
   2869                 }
   2870                 break;
   2871         }
   2872         
   2873         if (should_render) {
   2874             should_render = false;
   2875             rerender:
   2876             SDL_LockSurface(converted_background);
   2877             if (width == 160 && height == 144) {
   2878                 memcpy(pixels, converted_background->pixels, sizeof(pixels));
   2879             }
   2880             else {
   2881                 for (unsigned i = 0; i < width * height; i++) {
   2882                     pixels[i] = gui_palette_native[0];
   2883                 }
   2884                 for (unsigned y = 0; y < 144; y++) {
   2885                     memcpy(pixels + x_offset + width * (y + y_offset), ((uint32_t *)converted_background->pixels) + 160 * y, 160 * 4);
   2886                 }
   2887             }
   2888             SDL_UnlockSurface(converted_background);
   2889             
   2890             switch (gui_state) {
   2891                 case SHOWING_DROP_MESSAGE:
   2892                     draw_styled_text(pixels, width, height, 8 + y_offset, "Press ESC for menu", gui_palette_native[3], gui_palette_native[0], STYLE_CENTER);
   2893                     draw_styled_text(pixels, width, height, 116 + y_offset, "Drop a GB or GBC", gui_palette_native[3], gui_palette_native[0], STYLE_CENTER);
   2894                     draw_styled_text(pixels, width, height, 128 + y_offset, "file to play", gui_palette_native[3], gui_palette_native[0], STYLE_CENTER);
   2895                     break;
   2896                 case SHOWING_MENU:
   2897                     draw_styled_text(pixels, width, height, 8 + y_offset, "SameBoy", gui_palette_native[3], gui_palette_native[0], STYLE_LEFT);
   2898                     unsigned i = 0, y = 24;
   2899                     for (const struct menu_item *item = current_menu; item->string; item++, i++) {
   2900                         if (i == current_selection && !mouse_scroling) {
   2901                             if (i == 0) {
   2902                                 if (y < scroll) {
   2903                                     scroll = (y - 4) / 12 * 12;
   2904                                     goto rerender;
   2905                                 }
   2906                             }
   2907                             else {
   2908                                 if (y < scroll + 24) {
   2909                                     scroll = (y - 24) / 12 * 12;
   2910                                     goto rerender;
   2911                                 }
   2912                             }
   2913                         }
   2914                         if (i == current_selection && i == 0 && scroll != 0 && !mouse_scroling) {
   2915                             scroll = 0;
   2916                             goto rerender;
   2917                         }
   2918                         if (item->value_getter && !item->backwards_handler) {
   2919                             char line[25];
   2920                             snprintf(line, sizeof(line), "%s%*s", item->string, 23 - (unsigned)strlen(item->string), item->value_getter(i));
   2921                             draw_styled_text(pixels, width, height, y + y_offset, line, gui_palette_native[3], gui_palette_native[0],
   2922                                                i == current_selection ? STYLE_SELECTION : STYLE_INDENT);
   2923                             y += 12;
   2924                             
   2925                         }
   2926                         else {
   2927                             if (item->value_getter) {
   2928                                 draw_styled_text(pixels, width, height, y + y_offset, item->string, gui_palette_native[3], gui_palette_native[0],
   2929                                                  STYLE_LEFT);
   2930 
   2931                             }
   2932                             else {
   2933                                 draw_styled_text(pixels, width, height, y + y_offset, item->string, gui_palette_native[3], gui_palette_native[0],
   2934                                                    i == current_selection ? STYLE_SELECTION : STYLE_INDENT);
   2935                             }
   2936                             y += 12;
   2937                             if (item->value_getter) {
   2938                                 draw_styled_text(pixels, width, height, y + y_offset - 1, item->value_getter(i), gui_palette_native[3], gui_palette_native[0],
   2939                                                    i == current_selection ? STYLE_ARROWS : STYLE_CENTER);
   2940                                 y += 12;
   2941                             }
   2942                         }
   2943                         if (i == current_selection && !mouse_scroling) {
   2944                             if (y > scroll + 144) {
   2945                                 scroll = (y - 144) / 12 * 12;
   2946                                 if (scroll > menu_height - 144) {
   2947                                     scroll = menu_height - 144;
   2948                                 }
   2949                                 goto rerender;
   2950                             }
   2951                         }
   2952 
   2953                     }
   2954                     if (scrollbar_size) {
   2955                         unsigned scrollbar_offset = (140 - scrollbar_size) * scroll / (menu_height - 144);
   2956                         if (scrollbar_offset + scrollbar_size > 140) {
   2957                             scrollbar_offset = 140 - scrollbar_size;
   2958                         }
   2959                         for (unsigned y = 0; y < 140; y++) {
   2960                             uint32_t *pixel = pixels + x_offset + 156 + width * (y + y_offset + 2);
   2961                             if (y >= scrollbar_offset && y < scrollbar_offset + scrollbar_size) {
   2962                                 pixel[0] = pixel[1] = gui_palette_native[2];
   2963                             }
   2964                             else {
   2965                                 pixel[0] = pixel[1] = gui_palette_native[1];
   2966                             }
   2967                             
   2968                         }
   2969                     }
   2970                     break;
   2971                 case SHOWING_HELP:
   2972                     draw_text(pixels, width, height, 2 + x_offset, 2 + y_offset, help[current_help_page], gui_palette_native[3], gui_palette_native[0], false);
   2973                     break;
   2974                 case WAITING_FOR_KEY:
   2975                     draw_styled_text(pixels, width, height, 68 + y_offset, "Press a Key", gui_palette_native[3], gui_palette_native[0], STYLE_CENTER);
   2976                     break;
   2977                 case WAITING_FOR_JBUTTON:
   2978                     draw_styled_text(pixels, width, height, 68 + y_offset,
   2979                                        joypad_configuration_progress != JOYPAD_BUTTONS_MAX ? "Press button for" : "Move the Analog Stick",
   2980                                        gui_palette_native[3], gui_palette_native[0], STYLE_CENTER);
   2981                     draw_styled_text(pixels, width, height, 80 + y_offset,
   2982                                       GB_inline_const(const char *[], {
   2983                                            "Right",
   2984                                            "Left",
   2985                                            "Up",
   2986                                            "Down",
   2987                                            "A",
   2988                                            "B",
   2989                                            "Select",
   2990                                            "Start",
   2991                                            "Open Menu",
   2992                                            "Turbo",
   2993                                            "Rewind",
   2994                                            "Slow-Motion",
   2995                                            "Hotkey 1",
   2996                                            "Hotkey 2",
   2997                                            "Rapid A",
   2998                                            "Rapid B",
   2999                                            "",
   3000                                       }) [joypad_configuration_progress],
   3001                                       gui_palette_native[3], gui_palette_native[0], STYLE_CENTER);
   3002                     draw_styled_text(pixels, width, height, 104 + y_offset, "Press Enter to skip", gui_palette_native[3], gui_palette_native[0], STYLE_CENTER);
   3003                     break;
   3004                 case TEXT_INPUT:
   3005                     draw_styled_text(pixels, width, height, 32 + y_offset, text_input_title, gui_palette_native[3], gui_palette_native[0], STYLE_CENTER);
   3006                     draw_styled_text(pixels, width, height, 44 + y_offset, text_input_title2, gui_palette_native[3], gui_palette_native[0], STYLE_CENTER);
   3007                     draw_styled_text(pixels, width, height, 64 + y_offset, text_input, gui_palette_native[3], gui_palette_native[0], STYLE_CENTER);
   3008                     break;
   3009             }
   3010             
   3011             render_texture(pixels, NULL);
   3012 #ifdef _WIN32
   3013             /* Required for some Windows 10 machines, god knows why */
   3014             render_texture(pixels, NULL);
   3015 #endif
   3016         }
   3017     }
   3018 }
   3019 
   3020 static void __attribute__ ((constructor)) list_custom_palettes(void)
   3021 {
   3022     char *path = resource_path("Palettes");
   3023     if (!path) return;
   3024     if (strlen(path) > 1024 - 30) {
   3025         // path too long to safely concat filenames
   3026         return;
   3027     }
   3028     DIR *dir = opendir(path);
   3029     if (!dir) return;
   3030     
   3031     struct dirent *ent;
   3032     
   3033     while ((ent = readdir(dir))) {
   3034         unsigned length = strlen(ent->d_name);
   3035         if (length < 5 || length > 28) {
   3036             continue;
   3037         }
   3038         if (strcmp(ent->d_name + length - 4, ".sbp")) continue;
   3039         ent->d_name[length - 4] = 0;
   3040         custom_palettes = realloc(custom_palettes,
   3041                                   sizeof(custom_palettes[0]) * (n_custom_palettes + 1));
   3042         custom_palettes[n_custom_palettes++] = strdup(ent->d_name);
   3043     }
   3044     
   3045     closedir(dir);
   3046 }

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.