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.