SameBoy/SDL/gui.c

2250 lines
75 KiB
C

#include <OpenDialog/open_dialog.h>
#include <SDL.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include "utils.h"
#include "gui.h"
#include "font.h"
#include "audio/audio.h"
static const SDL_Color gui_palette[4] = {{8, 24, 16,}, {57, 97, 57,}, {132, 165, 99}, {198, 222, 140}};
static uint32_t gui_palette_native[4];
SDL_Window *window = NULL;
SDL_Renderer *renderer = NULL;
SDL_Texture *texture = NULL;
SDL_PixelFormat *pixel_format = NULL;
enum pending_command pending_command;
unsigned command_parameter;
char *dropped_state_file = NULL;
static char **custom_palettes;
static unsigned n_custom_palettes;
#ifdef __APPLE__
#define MODIFIER_NAME " " CMD_STRING
#else
#define MODIFIER_NAME CTRL_STRING
#endif
shader_t shader;
static SDL_Rect rect;
static unsigned factor;
static SDL_Surface *converted_background = NULL;
void render_texture(void *pixels, void *previous)
{
if (renderer) {
if (pixels) {
SDL_UpdateTexture(texture, NULL, pixels, GB_get_screen_width(&gb) * sizeof (uint32_t));
}
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
}
else {
static void *_pixels = NULL;
if (pixels) {
_pixels = pixels;
}
glClearColor(0, 0, 0, 1);
glClear(GL_COLOR_BUFFER_BIT);
GB_frame_blending_mode_t mode = configuration.blending_mode;
if (!previous) {
mode = GB_FRAME_BLENDING_MODE_DISABLED;
}
else if (mode == GB_FRAME_BLENDING_MODE_ACCURATE) {
if (GB_is_sgb(&gb)) {
mode = GB_FRAME_BLENDING_MODE_SIMPLE;
}
else {
mode = GB_is_odd_frame(&gb)? GB_FRAME_BLENDING_MODE_ACCURATE_ODD : GB_FRAME_BLENDING_MODE_ACCURATE_EVEN;
}
}
render_bitmap_with_shader(&shader, _pixels, previous,
GB_get_screen_width(&gb), GB_get_screen_height(&gb),
rect.x, rect.y, rect.w, rect.h,
mode);
SDL_GL_SwapWindow(window);
}
}
static const char *help[] = {
"Keyboard Shortcuts:\n"
" Open Menu: Escape\n"
" Open ROM: " MODIFIER_NAME "+O\n"
" Reset: " MODIFIER_NAME "+R\n"
" Pause: " MODIFIER_NAME "+P\n"
" Save state: " MODIFIER_NAME "+(0-9)\n"
" Load state: " MODIFIER_NAME "+" SHIFT_STRING "+(0-9)\n"
" Toggle Fullscreen " MODIFIER_NAME "+F\n"
#ifdef __APPLE__
" Mute/Unmute: " MODIFIER_NAME "+" SHIFT_STRING "+M\n"
#else
" Mute/Unmute: " MODIFIER_NAME "+M\n"
#endif
" Toggle channel: " ALT_STRING "+(1-4)\n"
" Break Debugger: " CTRL_STRING "+C",
"\n"
"SameBoy\n"
"Version " GB_VERSION "\n\n"
"Copyright " COPYRIGHT_STRING " 2015-" GB_COPYRIGHT_YEAR "\n"
"Lior Halphon\n\n"
"Licensed under the MIT\n"
"license, see LICENSE for\n"
"more details."
};
void update_viewport(void)
{
int win_width, win_height;
SDL_GL_GetDrawableSize(window, &win_width, &win_height);
int logical_width, logical_height;
SDL_GetWindowSize(window, &logical_width, &logical_height);
factor = win_width / logical_width;
double x_factor = win_width / (double) GB_get_screen_width(&gb);
double y_factor = win_height / (double) GB_get_screen_height(&gb);
if (configuration.scaling_mode == GB_SDL_SCALING_INTEGER_FACTOR) {
x_factor = (unsigned)(x_factor);
y_factor = (unsigned)(y_factor);
}
if (configuration.scaling_mode != GB_SDL_SCALING_ENTIRE_WINDOW) {
if (x_factor > y_factor) {
x_factor = y_factor;
}
else {
y_factor = x_factor;
}
}
unsigned new_width = x_factor * GB_get_screen_width(&gb);
unsigned new_height = y_factor * GB_get_screen_height(&gb);
rect = (SDL_Rect){(win_width - new_width) / 2, (win_height - new_height) /2,
new_width, new_height};
if (renderer) {
SDL_RenderSetViewport(renderer, &rect);
}
else {
glViewport(rect.x, rect.y, rect.w, rect.h);
}
}
static void rescale_window(void)
{
SDL_SetWindowSize(window, GB_get_screen_width(&gb) * configuration.default_scale, GB_get_screen_height(&gb) * configuration.default_scale);
}
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)
{
if (ch < ' ' || ch > font_max) {
ch = '?';
}
uint8_t *data = &font[(ch - ' ') * GLYPH_WIDTH * GLYPH_HEIGHT];
for (unsigned y = GLYPH_HEIGHT; y--;) {
for (unsigned x = GLYPH_WIDTH; x--;) {
if (*(data++) && buffer >= mask_top && buffer < mask_bottom) {
(*buffer) = color;
}
buffer++;
}
buffer += width - GLYPH_WIDTH;
}
}
static signed scroll = 0;
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)
{
if (!is_osd) {
y -= scroll;
}
unsigned orig_x = x;
unsigned y_offset = is_osd? 0 : (GB_get_screen_height(&gb) - 144) / 2;
while (*string) {
if (*string == '\n') {
x = orig_x;
y += GLYPH_HEIGHT + 4;
string++;
continue;
}
if (x > width - GLYPH_WIDTH) {
break;
}
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)]);
x += GLYPH_WIDTH;
string++;
}
}
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)
{
draw_unbordered_text(buffer, width, height, x - 1, y, string, border, is_osd);
draw_unbordered_text(buffer, width, height, x + 1, y, string, border, is_osd);
draw_unbordered_text(buffer, width, height, x, y - 1, string, border, is_osd);
draw_unbordered_text(buffer, width, height, x, y + 1, string, border, is_osd);
draw_unbordered_text(buffer, width, height, x, y, string, color, is_osd);
}
const char *osd_text = NULL;
unsigned osd_countdown = 0;
unsigned osd_text_lines = 1;
void show_osd_text(const char *text)
{
osd_text_lines = 1;
osd_text = text;
osd_countdown = 30;
while (*text++) {
if (*text == '\n') {
osd_text_lines++;
osd_countdown += 30;
}
}
}
enum decoration {
DECORATION_NONE,
DECORATION_SELECTION,
DECORATION_ARROWS,
};
static void draw_text_centered(uint32_t *buffer, unsigned width, unsigned height, unsigned y, const char *string, uint32_t color, uint32_t border, enum decoration decoration)
{
unsigned x = width / 2 - (unsigned) strlen(string) * GLYPH_WIDTH / 2;
draw_text(buffer, width, height, x, y, string, color, border, false);
switch (decoration) {
case DECORATION_SELECTION:
draw_text(buffer, width, height, x - GLYPH_WIDTH, y, SELECTION_STRING, color, border, false);
break;
case DECORATION_ARROWS:
draw_text(buffer, width, height, x - GLYPH_WIDTH, y, LEFT_ARROW_STRING, color, border, false);
draw_text(buffer, width, height, width - x, y, RIGHT_ARROW_STRING, color, border, false);
break;
case DECORATION_NONE:
break;
}
}
struct menu_item {
const char *string;
void (*handler)(unsigned);
const char *(*value_getter)(unsigned);
void (*backwards_handler)(unsigned);
};
static const struct menu_item *current_menu = NULL;
static const struct menu_item *root_menu = NULL;
static unsigned menu_height;
static unsigned scrollbar_size;
static bool mouse_scroling = false;
static unsigned current_selection = 0;
static enum {
SHOWING_DROP_MESSAGE,
SHOWING_MENU,
SHOWING_HELP,
WAITING_FOR_KEY,
WAITING_FOR_JBUTTON,
} gui_state;
static unsigned joypad_configuration_progress = 0;
static uint8_t joypad_axis_temp;
static void item_exit(unsigned index)
{
pending_command = GB_SDL_QUIT_COMMAND;
}
static unsigned current_help_page = 0;
static void item_help(unsigned index)
{
current_help_page = 0;
gui_state = SHOWING_HELP;
}
static void about(unsigned index)
{
current_help_page = 1;
gui_state = SHOWING_HELP;
}
static void enter_emulation_menu(unsigned index);
static void enter_graphics_menu(unsigned index);
static void enter_keyboard_menu(unsigned index);
static void enter_joypad_menu(unsigned index);
static void enter_audio_menu(unsigned index);
static void enter_controls_menu(unsigned index);
static void enter_help_menu(unsigned index);
static void enter_options_menu(unsigned index);
static void toggle_audio_recording(unsigned index);
extern void set_filename(const char *new_filename, typeof(free) *new_free_function);
static void open_rom(unsigned index)
{
char *filename = do_open_rom_dialog();
if (filename) {
set_filename(filename, free);
pending_command = GB_SDL_NEW_FILE_COMMAND;
}
}
static void cart_swap(unsigned index)
{
char *filename = do_open_rom_dialog();
if (filename) {
set_filename(filename, free);
pending_command = GB_SDL_CART_SWAP_COMMAND;
}
}
static void recalculate_menu_height(void)
{
menu_height = 24;
scrollbar_size = 0;
if (gui_state == SHOWING_MENU) {
for (const struct menu_item *item = current_menu; item->string; item++) {
menu_height += 12;
if (item->backwards_handler) {
menu_height += 12;
}
}
}
if (menu_height > 144) {
scrollbar_size = 144 * 140 / menu_height;
}
}
#if SDL_COMPILEDVERSION < 2014
int SDL_OpenURL(const char *url)
{
char *string = NULL;
#ifdef __APPLE__
asprintf(&string, "open '%s'", url);
#else
#ifdef _WIN32
asprintf(&string, "explorer '%s'", url);
#else
asprintf(&string, "xdg-open '%s'", url);
#endif
#endif
int ret = system(string);
free(string);
return ret;
}
#endif
static char audio_recording_menu_item[] = "Start Audio Recording";
static void sponsor(unsigned index)
{
SDL_OpenURL("https://github.com/sponsors/LIJI32");
}
static void debugger_help(unsigned index)
{
SDL_OpenURL("https://sameboy.github.io/debugger/");
}
static void return_to_root_menu(unsigned index)
{
current_menu = root_menu;
current_selection = 0;
scroll = 0;
recalculate_menu_height();
}
static const struct menu_item options_menu[] = {
{"Emulation Options", enter_emulation_menu},
{"Graphic Options", enter_graphics_menu},
{"Audio Options", enter_audio_menu},
{"Control Options", enter_controls_menu},
{"Back", return_to_root_menu},
{NULL,}
};
static void enter_options_menu(unsigned index)
{
current_menu = options_menu;
current_selection = 0;
scroll = 0;
recalculate_menu_height();
}
static const struct menu_item paused_menu[] = {
{"Resume", NULL},
{"Open ROM", open_rom},
{"Hot Swap Cartridge", cart_swap},
{"Options", enter_options_menu},
{audio_recording_menu_item, toggle_audio_recording},
{"Help & About", enter_help_menu},
{"Sponsor SameBoy", sponsor},
{"Quit SameBoy", item_exit},
{NULL,}
};
static struct menu_item nonpaused_menu[sizeof(paused_menu) / sizeof(paused_menu[0]) - 2];
static void __attribute__((constructor)) build_nonpaused_menu(void)
{
const struct menu_item *in = paused_menu;
struct menu_item *out = nonpaused_menu;
while (in->string) {
if (in->handler == NULL || in->handler == cart_swap) {
in++;
continue;
}
*out = *in;
out++;
in++;
}
}
static const struct menu_item help_menu[] = {
{"Shortcuts", item_help},
{"Debugger Help", debugger_help},
{"About SameBoy", about},
{"Back", return_to_root_menu},
{NULL,}
};
static void enter_help_menu(unsigned index)
{
current_menu = help_menu;
current_selection = 0;
scroll = 0;
recalculate_menu_height();
}
static void cycle_model(unsigned index)
{
configuration.model++;
if (configuration.model == MODEL_MAX) {
configuration.model = 0;
}
pending_command = GB_SDL_RESET_COMMAND;
}
static void cycle_model_backwards(unsigned index)
{
if (configuration.model == 0) {
configuration.model = MODEL_MAX;
}
configuration.model--;
pending_command = GB_SDL_RESET_COMMAND;
}
static const char *current_model_string(unsigned index)
{
return (const char *[]){"Game Boy", "Game Boy Color", "Game Boy Advance", "Super Game Boy", "Game Boy Pocket"}
[configuration.model];
}
static void cycle_cgb_revision(unsigned index)
{
if (configuration.cgb_revision == GB_MODEL_CGB_E - GB_MODEL_CGB_0) {
configuration.cgb_revision = 0;
}
else {
configuration.cgb_revision++;
}
pending_command = GB_SDL_RESET_COMMAND;
}
static void cycle_cgb_revision_backwards(unsigned index)
{
if (configuration.cgb_revision == 0) {
configuration.cgb_revision = GB_MODEL_CGB_E - GB_MODEL_CGB_0;
}
else {
configuration.cgb_revision--;
}
pending_command = GB_SDL_RESET_COMMAND;
}
static const char *current_cgb_revision_string(unsigned index)
{
return (const char *[]){
"CPU CGB 0 (Exp.)",
"CPU CGB A (Exp.)",
"CPU CGB B (Exp.)",
"CPU CGB C (Exp.)",
"CPU CGB D",
"CPU CGB E",
}
[configuration.cgb_revision];
}
static void cycle_sgb_revision(unsigned index)
{
configuration.sgb_revision++;
if (configuration.sgb_revision == SGB_MAX) {
configuration.sgb_revision = 0;
}
pending_command = GB_SDL_RESET_COMMAND;
}
static void cycle_sgb_revision_backwards(unsigned index)
{
if (configuration.sgb_revision == 0) {
configuration.sgb_revision = SGB_MAX;
}
configuration.sgb_revision--;
pending_command = GB_SDL_RESET_COMMAND;
}
static const char *current_sgb_revision_string(unsigned index)
{
return (const char *[]){"Super Game Boy NTSC", "Super Game Boy PAL", "Super Game Boy 2"}
[configuration.sgb_revision];
}
static const uint32_t rewind_lengths[] = {0, 10, 30, 60, 60 * 2, 60 * 5, 60 * 10};
static const char *rewind_strings[] = {"Disabled",
"10 Seconds",
"30 Seconds",
"1 Minute",
"2 Minutes",
"5 Minutes",
"10 Minutes",
};
static void cycle_rewind(unsigned index)
{
for (unsigned i = 0; i < sizeof(rewind_lengths) / sizeof(rewind_lengths[0]) - 1; i++) {
if (configuration.rewind_length == rewind_lengths[i]) {
configuration.rewind_length = rewind_lengths[i + 1];
GB_set_rewind_length(&gb, configuration.rewind_length);
return;
}
}
configuration.rewind_length = rewind_lengths[0];
GB_set_rewind_length(&gb, configuration.rewind_length);
}
static void cycle_rewind_backwards(unsigned index)
{
for (unsigned i = 1; i < sizeof(rewind_lengths) / sizeof(rewind_lengths[0]); i++) {
if (configuration.rewind_length == rewind_lengths[i]) {
configuration.rewind_length = rewind_lengths[i - 1];
GB_set_rewind_length(&gb, configuration.rewind_length);
return;
}
}
configuration.rewind_length = rewind_lengths[sizeof(rewind_lengths) / sizeof(rewind_lengths[0]) - 1];
GB_set_rewind_length(&gb, configuration.rewind_length);
}
static const char *current_rewind_string(unsigned index)
{
for (unsigned i = 0; i < sizeof(rewind_lengths) / sizeof(rewind_lengths[0]); i++) {
if (configuration.rewind_length == rewind_lengths[i]) {
return rewind_strings[i];
}
}
return "Custom";
}
static const char *current_bootrom_string(unsigned index)
{
if (!configuration.bootrom_path[0]) {
return "Built-in Boot ROMs";
}
size_t length = strlen(configuration.bootrom_path);
static char ret[24] = {0,};
if (length <= 23) {
strcpy(ret, configuration.bootrom_path);
}
else {
memcpy(ret, configuration.bootrom_path, 11);
memcpy(ret + 12, configuration.bootrom_path + length - 11, 11);
}
for (unsigned i = 0; i < 24; i++) {
if (ret[i] < 0) {
ret[i] = MOJIBAKE_STRING[0];
}
}
if (length > 23) {
ret[11] = ELLIPSIS_STRING[0];
}
return ret;
}
static void toggle_bootrom(unsigned index)
{
if (configuration.bootrom_path[0]) {
configuration.bootrom_path[0] = 0;
}
else {
char *folder = do_open_folder_dialog();
if (!folder) return;
if (strlen(folder) < sizeof(configuration.bootrom_path) - 1) {
strcpy(configuration.bootrom_path, folder);
}
free(folder);
}
}
static void toggle_rtc_mode(unsigned index)
{
configuration.rtc_mode = !configuration.rtc_mode;
}
static const char *current_rtc_mode_string(unsigned index)
{
switch (configuration.rtc_mode) {
case GB_RTC_MODE_SYNC_TO_HOST: return "Sync to System Clock";
case GB_RTC_MODE_ACCURATE: return "Accurate";
}
return "";
}
static void cycle_agb_revision(unsigned index)
{
configuration.agb_revision ^= GB_MODEL_GBP_BIT;
pending_command = GB_SDL_RESET_COMMAND;
}
static const char *current_agb_revision_string(unsigned index)
{
if (configuration.agb_revision == GB_MODEL_GBP_A) {
return "CPU AGB A (GBP)";
}
return "CPU AGB A (AGB)";
}
static const struct menu_item emulation_menu[] = {
{"Emulated Model:", cycle_model, current_model_string, cycle_model_backwards},
{"SGB Revision:", cycle_sgb_revision, current_sgb_revision_string, cycle_sgb_revision_backwards},
{"GBC Revision:", cycle_cgb_revision, current_cgb_revision_string, cycle_cgb_revision_backwards},
{"GBA Revision:", cycle_agb_revision, current_agb_revision_string, cycle_agb_revision},
{"Boot ROMs Folder:", toggle_bootrom, current_bootrom_string, toggle_bootrom},
{"Rewind Length:", cycle_rewind, current_rewind_string, cycle_rewind_backwards},
{"Real Time Clock:", toggle_rtc_mode, current_rtc_mode_string, toggle_rtc_mode},
{"Back", enter_options_menu},
{NULL,}
};
static void enter_emulation_menu(unsigned index)
{
current_menu = emulation_menu;
current_selection = 0;
scroll = 0;
recalculate_menu_height();
}
static const char *current_scaling_mode(unsigned index)
{
return (const char *[]){"Fill Entire Window", "Retain Aspect Ratio", "Retain Integer Factor"}
[configuration.scaling_mode];
}
static const char *current_default_scale(unsigned index)
{
return (const char *[]){"1x", "2x", "3x", "4x", "5x", "6x", "7x", "8x"}
[configuration.default_scale - 1];
}
const char *current_color_correction_mode(unsigned index)
{
return (const char *[]){"Disabled", "Correct Color Curves", "Modern - Balanced", "Modern - Boost Contrast", "Reduce Contrast", "Harsh Reality", "Modern - Accurate"}
[configuration.color_correction_mode];
}
const char *current_color_temperature(unsigned index)
{
static char ret[22];
strcpy(ret, SLIDER_STRING);
ret[configuration.color_temperature] = SELECTED_SLIDER_STRING[configuration.color_temperature];
return ret;
}
const char *current_palette(unsigned index)
{
if (configuration.dmg_palette == 4) {
return configuration.dmg_palette_name;
}
return (const char *[]){"Greyscale", "Lime (Game Boy)", "Olive (Pocket)", "Teal (Light)"}
[configuration.dmg_palette];
}
const char *current_border_mode(unsigned index)
{
return (const char *[]){"SGB Only", "Never", "Always"}
[configuration.border_mode];
}
static void cycle_scaling(unsigned index)
{
configuration.scaling_mode++;
if (configuration.scaling_mode == GB_SDL_SCALING_MAX) {
configuration.scaling_mode = 0;
}
update_viewport();
render_texture(NULL, NULL);
}
static void cycle_scaling_backwards(unsigned index)
{
if (configuration.scaling_mode == 0) {
configuration.scaling_mode = GB_SDL_SCALING_MAX - 1;
}
else {
configuration.scaling_mode--;
}
update_viewport();
render_texture(NULL, NULL);
}
static void cycle_default_scale(unsigned index)
{
if (configuration.default_scale == GB_SDL_DEFAULT_SCALE_MAX) {
configuration.default_scale = 1;
}
else {
configuration.default_scale++;
}
rescale_window();
update_viewport();
}
static void cycle_default_scale_backwards(unsigned index)
{
if (configuration.default_scale == 1) {
configuration.default_scale = GB_SDL_DEFAULT_SCALE_MAX;
}
else {
configuration.default_scale--;
}
rescale_window();
update_viewport();
}
static void cycle_color_correction(unsigned index)
{
if (configuration.color_correction_mode == GB_COLOR_CORRECTION_LOW_CONTRAST) {
configuration.color_correction_mode = GB_COLOR_CORRECTION_DISABLED;
}
else if (configuration.color_correction_mode == GB_COLOR_CORRECTION_MODERN_BALANCED) {
configuration.color_correction_mode = GB_COLOR_CORRECTION_MODERN_ACCURATE;
}
else if (configuration.color_correction_mode == GB_COLOR_CORRECTION_MODERN_ACCURATE) {
configuration.color_correction_mode = GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST;
}
else {
configuration.color_correction_mode++;
}
}
static void cycle_color_correction_backwards(unsigned index)
{
if (configuration.color_correction_mode == GB_COLOR_CORRECTION_DISABLED) {
configuration.color_correction_mode = GB_COLOR_CORRECTION_LOW_CONTRAST;
}
else if (configuration.color_correction_mode == GB_COLOR_CORRECTION_MODERN_ACCURATE) {
configuration.color_correction_mode = GB_COLOR_CORRECTION_MODERN_BALANCED;
}
else if (configuration.color_correction_mode == GB_COLOR_CORRECTION_MODERN_BOOST_CONTRAST) {
configuration.color_correction_mode = GB_COLOR_CORRECTION_MODERN_ACCURATE;
}
else {
configuration.color_correction_mode--;
}
}
static void decrease_color_temperature(unsigned index)
{
if (configuration.color_temperature < 20) {
configuration.color_temperature++;
}
}
static void increase_color_temperature(unsigned index)
{
if (configuration.color_temperature > 0) {
configuration.color_temperature--;
}
}
const GB_palette_t *current_dmg_palette(void)
{
typedef struct __attribute__ ((packed)) {
uint32_t magic;
uint8_t flags;
struct GB_color_s colors[5];
int32_t brightness_bias;
uint32_t hue_bias;
uint32_t hue_bias_strength;
} theme_t;
static theme_t theme;
if (configuration.dmg_palette == 4) {
char *path = resource_path("Palettes");
sprintf(path + strlen(path), "/%s.sbp", configuration.dmg_palette_name);
FILE *file = fopen(path, "rb");
if (!file) return &GB_PALETTE_GREY;
memset(&theme, 0, sizeof(theme));
fread(&theme, sizeof(theme), 1, file);
fclose(file);
#ifdef GB_BIG_ENDIAN
theme.magic = __builtin_bswap32(theme.magic);
#endif
if (theme.magic != 'SBPL') return &GB_PALETTE_GREY;
return (GB_palette_t *)&theme.colors;
}
switch (configuration.dmg_palette) {
case 1: return &GB_PALETTE_DMG;
case 2: return &GB_PALETTE_MGB;
case 3: return &GB_PALETTE_GBL;
default: return &GB_PALETTE_GREY;
}
}
static void update_gui_palette(void)
{
const GB_palette_t *palette = current_dmg_palette();
SDL_Color colors[4];
for (unsigned i = 4; i--; ) {
gui_palette_native[i] = SDL_MapRGB(pixel_format, palette->colors[i].r, palette->colors[i].g, palette->colors[i].b);
colors[i].r = palette->colors[i].r;
colors[i].g = palette->colors[i].g;
colors[i].b = palette->colors[i].b;
}
SDL_Surface *background = SDL_LoadBMP(resource_path("background.bmp"));
/* Create a blank background if background.bmp could not be loaded */
if (!background) {
background = SDL_CreateRGBSurface(0, 160, 144, 8, 0, 0, 0, 0);
}
SDL_SetPaletteColors(background->format->palette, colors, 0, 4);
converted_background = SDL_ConvertSurface(background, pixel_format, 0);
SDL_FreeSurface(background);
}
static void cycle_palette(unsigned index)
{
if (configuration.dmg_palette == 3) {
if (n_custom_palettes == 0) {
configuration.dmg_palette = 0;
}
else {
configuration.dmg_palette = 4;
strcpy(configuration.dmg_palette_name, custom_palettes[0]);
}
}
else if (configuration.dmg_palette == 4) {
for (unsigned i = 0; i < n_custom_palettes; i++) {
if (strcmp(custom_palettes[i], configuration.dmg_palette_name) == 0) {
if (i == n_custom_palettes - 1) {
configuration.dmg_palette = 0;
}
else {
strcpy(configuration.dmg_palette_name, custom_palettes[i + 1]);
}
break;
}
}
}
else {
configuration.dmg_palette++;
}
configuration.gui_pallete_enabled = true;
update_gui_palette();
}
static void cycle_palette_backwards(unsigned index)
{
if (configuration.dmg_palette == 0) {
if (n_custom_palettes == 0) {
configuration.dmg_palette = 3;
}
else {
configuration.dmg_palette = 4;
strcpy(configuration.dmg_palette_name, custom_palettes[n_custom_palettes - 1]);
}
}
else if (configuration.dmg_palette == 4) {
for (unsigned i = 0; i < n_custom_palettes; i++) {
if (strcmp(custom_palettes[i], configuration.dmg_palette_name) == 0) {
if (i == 0) {
configuration.dmg_palette = 3;
}
else {
strcpy(configuration.dmg_palette_name, custom_palettes[i - 1]);
}
break;
}
}
}
else {
configuration.dmg_palette--;
}
configuration.gui_pallete_enabled = true;
update_gui_palette();
}
static void cycle_border_mode(unsigned index)
{
if (configuration.border_mode == GB_BORDER_ALWAYS) {
configuration.border_mode = GB_BORDER_SGB;
}
else {
configuration.border_mode++;
}
}
static void cycle_border_mode_backwards(unsigned index)
{
if (configuration.border_mode == GB_BORDER_SGB) {
configuration.border_mode = GB_BORDER_ALWAYS;
}
else {
configuration.border_mode--;
}
}
extern bool uses_gl(void);
struct shader_name {
const char *file_name;
const char *display_name;
} shaders[] =
{
{"NearestNeighbor", "Nearest Neighbor"},
{"Bilinear", "Bilinear"},
{"SmoothBilinear", "Smooth Bilinear"},
{"MonoLCD", "Monochrome LCD"},
{"LCD", "LCD Display"},
{"CRT", "CRT Display"},
{"Scale2x", "Scale2x"},
{"Scale4x", "Scale4x"},
{"AAScale2x", "Anti-aliased Scale2x"},
{"AAScale4x", "Anti-aliased Scale4x"},
{"HQ2x", "HQ2x"},
{"OmniScale", "OmniScale"},
{"OmniScaleLegacy", "OmniScale Legacy"},
{"AAOmniScaleLegacy", "AA OmniScale Legacy"},
};
static void cycle_filter(unsigned index)
{
if (!uses_gl()) return;
unsigned i = 0;
for (; i < sizeof(shaders) / sizeof(shaders[0]); i++) {
if (strcmp(shaders[i].file_name, configuration.filter) == 0) {
break;
}
}
i += 1;
if (i >= sizeof(shaders) / sizeof(shaders[0])) {
i -= sizeof(shaders) / sizeof(shaders[0]);
}
strcpy(configuration.filter, shaders[i].file_name);
free_shader(&shader);
if (!init_shader_with_name(&shader, configuration.filter)) {
init_shader_with_name(&shader, "NearestNeighbor");
}
}
static void cycle_filter_backwards(unsigned index)
{
if (!uses_gl()) return;
unsigned i = 0;
for (; i < sizeof(shaders) / sizeof(shaders[0]); i++) {
if (strcmp(shaders[i].file_name, configuration.filter) == 0) {
break;
}
}
i -= 1;
if (i >= sizeof(shaders) / sizeof(shaders[0])) {
i = sizeof(shaders) / sizeof(shaders[0]) - 1;
}
strcpy(configuration.filter, shaders[i].file_name);
free_shader(&shader);
if (!init_shader_with_name(&shader, configuration.filter)) {
init_shader_with_name(&shader, "NearestNeighbor");
}
}
static const char *current_filter_name(unsigned index)
{
if (!uses_gl()) return "Requires OpenGL 3.2+";
unsigned i = 0;
for (; i < sizeof(shaders) / sizeof(shaders[0]); i++) {
if (strcmp(shaders[i].file_name, configuration.filter) == 0) {
break;
}
}
if (i == sizeof(shaders) / sizeof(shaders[0])) {
i = 0;
}
return shaders[i].display_name;
}
static void cycle_blending_mode(unsigned index)
{
if (!uses_gl()) return;
if (configuration.blending_mode == GB_FRAME_BLENDING_MODE_ACCURATE) {
configuration.blending_mode = GB_FRAME_BLENDING_MODE_DISABLED;
}
else {
configuration.blending_mode++;
}
}
static void cycle_blending_mode_backwards(unsigned index)
{
if (!uses_gl()) return;
if (configuration.blending_mode == GB_FRAME_BLENDING_MODE_DISABLED) {
configuration.blending_mode = GB_FRAME_BLENDING_MODE_ACCURATE;
}
else {
configuration.blending_mode--;
}
}
static const char *blending_mode_string(unsigned index)
{
if (!uses_gl()) return "Requires OpenGL 3.2+";
return (const char *[]){"Disabled", "Simple", "Accurate"}
[configuration.blending_mode];
}
static void toggle_osd(unsigned index)
{
osd_countdown = 0;
configuration.osd = !configuration.osd;
}
static const char *current_osd_mode(unsigned index)
{
return configuration.osd? "Enabled" : "Disabled";
}
static const struct menu_item graphics_menu[] = {
{"Scaling Mode:", cycle_scaling, current_scaling_mode, cycle_scaling_backwards},
{"Default Window Scale:", cycle_default_scale, current_default_scale, cycle_default_scale_backwards},
{"Scaling Filter:", cycle_filter, current_filter_name, cycle_filter_backwards},
{"Color Correction:", cycle_color_correction, current_color_correction_mode, cycle_color_correction_backwards},
{"Ambient Light Temp.:", decrease_color_temperature, current_color_temperature, increase_color_temperature},
{"Frame Blending:", cycle_blending_mode, blending_mode_string, cycle_blending_mode_backwards},
{"Mono Palette:", cycle_palette, current_palette, cycle_palette_backwards},
{"Display Border:", cycle_border_mode, current_border_mode, cycle_border_mode_backwards},
{"On-Screen Display:", toggle_osd, current_osd_mode, toggle_osd},
{"Back", enter_options_menu},
{NULL,}
};
static void enter_graphics_menu(unsigned index)
{
current_menu = graphics_menu;
current_selection = 0;
scroll = 0;
recalculate_menu_height();
}
static const char *highpass_filter_string(unsigned index)
{
return (const char *[]){"None (Keep DC Offset)", "Accurate", "Preserve Waveform"}
[configuration.highpass_mode];
}
static void cycle_highpass_filter(unsigned index)
{
configuration.highpass_mode++;
if (configuration.highpass_mode == GB_HIGHPASS_MAX) {
configuration.highpass_mode = 0;
}
}
static void cycle_highpass_filter_backwards(unsigned index)
{
if (configuration.highpass_mode == 0) {
configuration.highpass_mode = GB_HIGHPASS_MAX - 1;
}
else {
configuration.highpass_mode--;
}
}
static const char *volume_string(unsigned index)
{
static char ret[5];
sprintf(ret, "%d%%", configuration.volume);
return ret;
}
static void increase_volume(unsigned index)
{
configuration.volume += 5;
if (configuration.volume > 100) {
configuration.volume = 100;
}
}
static void decrease_volume(unsigned index)
{
configuration.volume -= 5;
if (configuration.volume > 100) {
configuration.volume = 0;
}
}
static const char *interference_volume_string(unsigned index)
{
static char ret[5];
sprintf(ret, "%d%%", configuration.interference_volume);
return ret;
}
static void increase_interference_volume(unsigned index)
{
configuration.interference_volume += 5;
if (configuration.interference_volume > 100) {
configuration.interference_volume = 100;
}
}
static void decrease_interference_volume(unsigned index)
{
configuration.interference_volume -= 5;
if (configuration.interference_volume > 100) {
configuration.interference_volume = 0;
}
}
static const char *audio_driver_string(unsigned index)
{
return GB_audio_driver_name();
}
static const char *preferred_audio_driver_string(unsigned index)
{
if (configuration.audio_driver[0] == 0) {
return "Auto";
}
return configuration.audio_driver;
}
static void audio_driver_changed(void);
static void cycle_prefrered_audio_driver(unsigned index)
{
audio_driver_changed();
if (configuration.audio_driver[0] == 0) {
strcpy(configuration.audio_driver, GB_audio_driver_name_at_index(0));
return;
}
unsigned i = 0;
while (true) {
const char *name = GB_audio_driver_name_at_index(i);
if (name[0] == 0) { // Not a supported driver? Switch to auto
configuration.audio_driver[0] = 0;
return;
}
if (strcmp(configuration.audio_driver, name) == 0) {
strcpy(configuration.audio_driver, GB_audio_driver_name_at_index(i + 1));
return;
}
i++;
}
}
static void cycle_preferred_audio_driver_backwards(unsigned index)
{
audio_driver_changed();
if (configuration.audio_driver[0] == 0) {
unsigned i = 0;
while (true) {
const char *name = GB_audio_driver_name_at_index(i);
if (name[0] == 0) {
strcpy(configuration.audio_driver, GB_audio_driver_name_at_index(i - 1));
return;
}
i++;
}
return;
}
unsigned i = 0;
while (true) {
const char *name = GB_audio_driver_name_at_index(i);
if (name[0] == 0) { // Not a supported driver? Switch to auto
configuration.audio_driver[0] = 0;
return;
}
if (strcmp(configuration.audio_driver, name) == 0) {
strcpy(configuration.audio_driver, GB_audio_driver_name_at_index(i - 1));
return;
}
i++;
}
}
static void nop(unsigned index){}
static struct menu_item audio_menu[] = {
{"Highpass Filter:", cycle_highpass_filter, highpass_filter_string, cycle_highpass_filter_backwards},
{"Volume:", increase_volume, volume_string, decrease_volume},
{"Interference Volume:", increase_interference_volume, interference_volume_string, decrease_interference_volume},
{"Preferred Audio Driver:", cycle_prefrered_audio_driver, preferred_audio_driver_string, cycle_preferred_audio_driver_backwards},
{"Active Driver:", nop, audio_driver_string},
{"Back", enter_options_menu},
{NULL,}
};
static void audio_driver_changed(void)
{
audio_menu[4].value_getter = NULL;
audio_menu[4].string = "Relaunch to apply";
}
static void enter_audio_menu(unsigned index)
{
current_menu = audio_menu;
current_selection = 0;
scroll = 0;
recalculate_menu_height();
}
static void modify_key(unsigned index)
{
gui_state = WAITING_FOR_KEY;
}
static const char *key_name(unsigned index);
static const struct menu_item keyboard_menu[] = {
{"Right:", modify_key, key_name,},
{"Left:", modify_key, key_name,},
{"Up:", modify_key, key_name,},
{"Down:", modify_key, key_name,},
{"A:", modify_key, key_name,},
{"B:", modify_key, key_name,},
{"Select:", modify_key, key_name,},
{"Start:", modify_key, key_name,},
{"Turbo:", modify_key, key_name,},
{"Rewind:", modify_key, key_name,},
{"Slow-Motion:", modify_key, key_name,},
{"Back", enter_controls_menu},
{NULL,}
};
static const char *key_name(unsigned index)
{
if (index > 8) {
return SDL_GetScancodeName(configuration.keys_2[index - 9]);
}
return SDL_GetScancodeName(configuration.keys[index]);
}
static void enter_keyboard_menu(unsigned index)
{
current_menu = keyboard_menu;
current_selection = 0;
scroll = 0;
recalculate_menu_height();
}
static unsigned joypad_index = 0;
static SDL_Joystick *joystick = NULL;
static SDL_GameController *controller = NULL;
SDL_Haptic *haptic = NULL;
static const char *current_joypad_name(unsigned index)
{
static char name[23] = {0,};
const char *orig_name = joystick? SDL_JoystickName(joystick) : NULL;
if (!orig_name) return "Not Found";
unsigned i = 0;
// SDL returns a name with repeated and trailing spaces
while (*orig_name && i < sizeof(name) - 2) {
if (orig_name[0] != ' ' || orig_name[1] != ' ') {
name[i++] = *orig_name > 0? *orig_name : MOJIBAKE_STRING[0];
}
orig_name++;
}
if (i && name[i - 1] == ' ') {
i--;
}
name[i] = 0;
return name;
}
static void cycle_joypads(unsigned index)
{
joypad_index++;
if (joypad_index >= SDL_NumJoysticks()) {
joypad_index = 0;
}
if (haptic) {
SDL_HapticClose(haptic);
haptic = NULL;
}
if (controller) {
SDL_GameControllerClose(controller);
controller = NULL;
}
else if (joystick) {
SDL_JoystickClose(joystick);
joystick = NULL;
}
if ((controller = SDL_GameControllerOpen(joypad_index))) {
joystick = SDL_GameControllerGetJoystick(controller);
}
else {
joystick = SDL_JoystickOpen(joypad_index);
}
if (joystick) {
haptic = SDL_HapticOpenFromJoystick(joystick);
}}
static void cycle_joypads_backwards(unsigned index)
{
joypad_index--;
if (joypad_index >= SDL_NumJoysticks()) {
joypad_index = SDL_NumJoysticks() - 1;
}
if (haptic) {
SDL_HapticClose(haptic);
haptic = NULL;
}
if (controller) {
SDL_GameControllerClose(controller);
controller = NULL;
}
else if (joystick) {
SDL_JoystickClose(joystick);
joystick = NULL;
}
if ((controller = SDL_GameControllerOpen(joypad_index))) {
joystick = SDL_GameControllerGetJoystick(controller);
}
else {
joystick = SDL_JoystickOpen(joypad_index);
}
if (joystick) {
haptic = SDL_HapticOpenFromJoystick(joystick);
}}
static void detect_joypad_layout(unsigned index)
{
gui_state = WAITING_FOR_JBUTTON;
joypad_configuration_progress = 0;
joypad_axis_temp = -1;
}
static void cycle_rumble_mode(unsigned index)
{
if (configuration.rumble_mode == GB_RUMBLE_ALL_GAMES) {
configuration.rumble_mode = GB_RUMBLE_DISABLED;
}
else {
configuration.rumble_mode++;
}
}
static void cycle_rumble_mode_backwards(unsigned index)
{
if (configuration.rumble_mode == GB_RUMBLE_DISABLED) {
configuration.rumble_mode = GB_RUMBLE_ALL_GAMES;
}
else {
configuration.rumble_mode--;
}
}
static const char *current_rumble_mode(unsigned index)
{
return (const char *[]){"Disabled", "Rumble Game Paks Only", "All Games"}
[configuration.rumble_mode];
}
static void toggle_allow_background_controllers(unsigned index)
{
configuration.allow_background_controllers ^= true;
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS,
configuration.allow_background_controllers? "1" : "0");
}
static const char *current_background_control_mode(unsigned index)
{
return configuration.allow_background_controllers? "Always" : "During Window Focus Only";
}
static void cycle_hotkey(unsigned index)
{
if (configuration.hotkey_actions[index - 2] == HOTKEY_MAX) {
configuration.hotkey_actions[index - 2] = 0;
}
else {
configuration.hotkey_actions[index - 2]++;
}
}
static void cycle_hotkey_backwards(unsigned index)
{
if (configuration.hotkey_actions[index - 2] == 0) {
configuration.hotkey_actions[index - 2] = HOTKEY_MAX;
}
else {
configuration.hotkey_actions[index - 2]--;
}
}
static const char *current_hotkey(unsigned index)
{
return (const char *[]){
"None",
"Toggle Pause",
"Toggle Mute",
"Reset",
"Quit SameBoy",
"Save State Slot 1",
"Load State Slot 1",
"Save State Slot 2",
"Load State Slot 2",
"Save State Slot 3",
"Load State Slot 3",
"Save State Slot 4",
"Load State Slot 4",
"Save State Slot 5",
"Load State Slot 5",
"Save State Slot 6",
"Load State Slot 6",
"Save State Slot 7",
"Load State Slot 7",
"Save State Slot 8",
"Load State Slot 8",
"Save State Slot 9",
"Load State Slot 9",
"Save State Slot 10",
"Load State Slot 10",
}
[configuration.hotkey_actions[index - 2]];
}
static const struct menu_item joypad_menu[] = {
{"Joypad:", cycle_joypads, current_joypad_name, cycle_joypads_backwards},
{"Configure layout", detect_joypad_layout},
{"Hotkey 1 Action:", cycle_hotkey, current_hotkey, cycle_hotkey_backwards},
{"Hotkey 2 Action:", cycle_hotkey, current_hotkey, cycle_hotkey_backwards},
{"Rumble Mode:", cycle_rumble_mode, current_rumble_mode, cycle_rumble_mode_backwards},
{"Enable Control:", toggle_allow_background_controllers, current_background_control_mode, toggle_allow_background_controllers},
{"Back", enter_controls_menu},
{NULL,}
};
static void enter_joypad_menu(unsigned index)
{
current_menu = joypad_menu;
current_selection = 0;
scroll = 0;
recalculate_menu_height();
}
joypad_button_t get_joypad_button(uint8_t physical_button)
{
for (unsigned i = 0; i < JOYPAD_BUTTONS_MAX; i++) {
if (configuration.joypad_configuration[i] == physical_button) {
return i;
}
}
return JOYPAD_BUTTONS_MAX;
}
joypad_axis_t get_joypad_axis(uint8_t physical_axis)
{
for (unsigned i = 0; i < JOYPAD_AXISES_MAX; i++) {
if (configuration.joypad_axises[i] == physical_axis) {
return i;
}
}
return JOYPAD_AXISES_MAX;
}
void connect_joypad(void)
{
if (joystick && !SDL_NumJoysticks()) {
if (controller) {
SDL_GameControllerClose(controller);
controller = NULL;
joystick = NULL;
}
else {
SDL_JoystickClose(joystick);
joystick = NULL;
}
}
else if (!joystick && SDL_NumJoysticks()) {
if ((controller = SDL_GameControllerOpen(0))) {
joystick = SDL_GameControllerGetJoystick(controller);
}
else {
joystick = SDL_JoystickOpen(0);
}
}
if (joystick) {
haptic = SDL_HapticOpenFromJoystick(joystick);
}
}
static void toggle_mouse_control(unsigned index)
{
configuration.allow_mouse_controls = !configuration.allow_mouse_controls;
}
static const char *mouse_control_string(unsigned index)
{
return configuration.allow_mouse_controls? "Allow mouse control" : "Disallow mouse control";
}
static const struct menu_item controls_menu[] = {
{"Keyboard Options", enter_keyboard_menu},
{"Joypad Options", enter_joypad_menu},
{"Motion-controlled games:", toggle_mouse_control, mouse_control_string, toggle_mouse_control},
{"Back", enter_options_menu},
{NULL,}
};
static void enter_controls_menu(unsigned index)
{
current_menu = controls_menu;
current_selection = 0;
scroll = 0;
recalculate_menu_height();
}
static void toggle_audio_recording(unsigned index)
{
if (!GB_is_inited(&gb)) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", "Cannot start audio recording, open a ROM file first.", window);
return;
}
static bool is_recording = false;
if (is_recording) {
is_recording = false;
show_osd_text("Audio recording ended");
int error = GB_stop_audio_recording(&gb);
if (error) {
char *message = NULL;
asprintf(&message, "Could not finalize recording: %s", strerror(error));
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", message, window);
free(message);
}
static const char item_string[] = "Start Audio Recording";
memcpy(audio_recording_menu_item, item_string, sizeof(item_string));
return;
}
char *filename = do_save_recording_dialog(GB_get_sample_rate(&gb));
/* Drop events as it SDL seems to catch several in-dialog events */
SDL_Event event;
while (SDL_PollEvent(&event));
if (filename) {
GB_audio_format_t format = GB_AUDIO_FORMAT_RAW;
size_t length = strlen(filename);
if (length >= 5) {
if (strcasecmp(".aiff", filename + length - 5) == 0) {
format = GB_AUDIO_FORMAT_AIFF;
}
else if (strcasecmp(".aifc", filename + length - 5) == 0) {
format = GB_AUDIO_FORMAT_AIFF;
}
else if (length >= 4) {
if (strcasecmp(".aif", filename + length - 4) == 0) {
format = GB_AUDIO_FORMAT_AIFF;
}
else if (strcasecmp(".wav", filename + length - 4) == 0) {
format = GB_AUDIO_FORMAT_WAV;
}
}
}
int error = GB_start_audio_recording(&gb, filename, format);
free(filename);
if (error) {
char *message = NULL;
asprintf(&message, "Could not finalize recording: %s", strerror(error));
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Error", message, window);
free(message);
return;
}
is_recording = true;
static const char item_string[] = "Stop Audio Recording";
memcpy(audio_recording_menu_item, item_string, sizeof(item_string));
show_osd_text("Audio recording started");
}
}
void convert_mouse_coordinates(signed *x, signed *y)
{
signed width = GB_get_screen_width(&gb);
signed height = GB_get_screen_height(&gb);
signed x_offset = (width - 160) / 2;
signed y_offset = (height - 144) / 2;
*x = (signed)(*x - rect.x / factor) * width / (signed)(rect.w / factor) - x_offset;
*y = (signed)(*y - rect.y / factor) * height / (signed)(rect.h / factor) - y_offset;
if (strcmp("CRT", configuration.filter) == 0) {
*y = *y * 8 / 7;
*y -= 144 / 16;
}
}
void run_gui(bool is_running)
{
SDL_ShowCursor(SDL_ENABLE);
connect_joypad();
/* Draw the background screen */
if (!converted_background) {
if (configuration.gui_pallete_enabled) {
update_gui_palette();
}
else {
SDL_Surface *background = SDL_LoadBMP(resource_path("background.bmp"));
/* Create a blank background if background.bmp could not be loaded */
if (!background) {
background = SDL_CreateRGBSurface(0, 160, 144, 8, 0, 0, 0, 0);
}
SDL_SetPaletteColors(background->format->palette, gui_palette, 0, 4);
converted_background = SDL_ConvertSurface(background, pixel_format, 0);
SDL_FreeSurface(background);
for (unsigned i = 4; i--; ) {
gui_palette_native[i] = SDL_MapRGB(pixel_format, gui_palette[i].r, gui_palette[i].g, gui_palette[i].b);
}
}
}
unsigned width = GB_get_screen_width(&gb);
unsigned height = GB_get_screen_height(&gb);
unsigned x_offset = (width - 160) / 2;
unsigned y_offset = (height - 144) / 2;
uint32_t pixels[width * height];
if (width != 160 || height != 144) {
for (unsigned i = 0; i < width * height; i++) {
pixels[i] = gui_palette_native[0];
}
}
SDL_Event event = {0,};
gui_state = is_running? SHOWING_MENU : SHOWING_DROP_MESSAGE;
bool should_render = true;
current_menu = root_menu = is_running? paused_menu : nonpaused_menu;
recalculate_menu_height();
current_selection = 0;
scroll = 0;
bool scrollbar_drag = false;
signed scroll_mouse_start = 0;
signed scroll_start = 0;
while (true) {
SDL_WaitEvent(&event);
/* Convert Joypad and mouse events (We only generate down events) */
if (gui_state != WAITING_FOR_KEY && gui_state != WAITING_FOR_JBUTTON) {
switch (event.type) {
case SDL_KEYDOWN:
if (gui_state == WAITING_FOR_KEY) break;
if (event.key.keysym.mod != 0) break;
switch (event.key.keysym.scancode) {
// Do not remap these keys to prevent deadlocking
case SDL_SCANCODE_ESCAPE:
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_RIGHT:
case SDL_SCANCODE_LEFT:
case SDL_SCANCODE_UP:
case SDL_SCANCODE_DOWN:
break;
default:
if (event.key.keysym.scancode == configuration.keys[GB_KEY_RIGHT]) event.key.keysym.scancode = SDL_SCANCODE_RIGHT;
else if (event.key.keysym.scancode == configuration.keys[GB_KEY_LEFT]) event.key.keysym.scancode = SDL_SCANCODE_LEFT;
else if (event.key.keysym.scancode == configuration.keys[GB_KEY_UP]) event.key.keysym.scancode = SDL_SCANCODE_UP;
else if (event.key.keysym.scancode == configuration.keys[GB_KEY_DOWN]) event.key.keysym.scancode = SDL_SCANCODE_DOWN;
else if (event.key.keysym.scancode == configuration.keys[GB_KEY_A]) event.key.keysym.scancode = SDL_SCANCODE_RETURN;
else if (event.key.keysym.scancode == configuration.keys[GB_KEY_START]) event.key.keysym.scancode = SDL_SCANCODE_RETURN;
else if (event.key.keysym.scancode == configuration.keys[GB_KEY_B]) event.key.keysym.scancode = SDL_SCANCODE_ESCAPE;
break;
}
break;
case SDL_WINDOWEVENT:
should_render = true;
break;
case SDL_MOUSEBUTTONUP:
scrollbar_drag = false;
break;
case SDL_MOUSEBUTTONDOWN:
if (gui_state == SHOWING_HELP) {
event.type = SDL_KEYDOWN;
event.key.keysym.scancode = SDL_SCANCODE_RETURN;
}
else if (gui_state == SHOWING_DROP_MESSAGE) {
event.type = SDL_KEYDOWN;
event.key.keysym.scancode = SDL_SCANCODE_ESCAPE;
}
else if (gui_state == SHOWING_MENU) {
signed x = event.button.x;
signed y = event.button.y;
convert_mouse_coordinates(&x, &y);
if (x >= 160 - 6 && x < 160 && menu_height > 144) {
unsigned scrollbar_offset = (140 - scrollbar_size) * scroll / (menu_height - 144);
if (scrollbar_offset + scrollbar_size > 140) {
scrollbar_offset = 140 - scrollbar_size;
}
if (y < scrollbar_offset || y > scrollbar_offset + scrollbar_size) {
scroll = (menu_height - 144) * y / 143;
should_render = true;
}
scrollbar_drag = true;
mouse_scroling = true;
scroll_mouse_start = y;
scroll_start = scroll;
break;
}
y += scroll;
if (x < 0 || x >= 160 || y < 24) {
continue;
}
unsigned item_y = 24;
unsigned index = 0;
for (const struct menu_item *item = current_menu; item->string; item++, index++) {
if (!item->backwards_handler) {
if (y >= item_y && y < item_y + 12) {
break;
}
item_y += 12;
}
else {
if (y >= item_y && y < item_y + 24) {
break;
}
item_y += 24;
}
}
if (!current_menu[index].string) continue;
current_selection = index;
event.type = SDL_KEYDOWN;
if (current_menu[index].backwards_handler) {
event.key.keysym.scancode = x < 80? SDL_SCANCODE_LEFT : SDL_SCANCODE_RIGHT;
}
else {
event.key.keysym.scancode = SDL_SCANCODE_RETURN;
}
}
break;
case SDL_JOYBUTTONDOWN:
event.type = SDL_KEYDOWN;
joypad_button_t button = get_joypad_button(event.jbutton.button);
if (button == JOYPAD_BUTTON_A) {
event.key.keysym.scancode = SDL_SCANCODE_RETURN;
}
else if (button == JOYPAD_BUTTON_MENU || button == JOYPAD_BUTTON_B) {
event.key.keysym.scancode = SDL_SCANCODE_ESCAPE;
}
else if (button == JOYPAD_BUTTON_UP) event.key.keysym.scancode = SDL_SCANCODE_UP;
else if (button == JOYPAD_BUTTON_DOWN) event.key.keysym.scancode = SDL_SCANCODE_DOWN;
else if (button == JOYPAD_BUTTON_LEFT) event.key.keysym.scancode = SDL_SCANCODE_LEFT;
else if (button == JOYPAD_BUTTON_RIGHT) event.key.keysym.scancode = SDL_SCANCODE_RIGHT;
break;
case SDL_JOYHATMOTION: {
uint8_t value = event.jhat.value;
if (value != 0) {
uint32_t scancode =
value == SDL_HAT_UP ? SDL_SCANCODE_UP
: value == SDL_HAT_DOWN ? SDL_SCANCODE_DOWN
: value == SDL_HAT_LEFT ? SDL_SCANCODE_LEFT
: value == SDL_HAT_RIGHT ? SDL_SCANCODE_RIGHT
: 0;
if (scancode != 0) {
event.type = SDL_KEYDOWN;
event.key.keysym.scancode = scancode;
}
}
break;
}
case SDL_JOYAXISMOTION: {
static bool axis_active[2] = {false, false};
joypad_axis_t axis = get_joypad_axis(event.jaxis.axis);
if (axis == JOYPAD_AXISES_X) {
if (!axis_active[0] && event.jaxis.value > JOYSTICK_HIGH) {
axis_active[0] = true;
event.type = SDL_KEYDOWN;
event.key.keysym.scancode = SDL_SCANCODE_RIGHT;
}
else if (!axis_active[0] && event.jaxis.value < -JOYSTICK_HIGH) {
axis_active[0] = true;
event.type = SDL_KEYDOWN;
event.key.keysym.scancode = SDL_SCANCODE_LEFT;
}
else if (axis_active[0] && event.jaxis.value < JOYSTICK_LOW && event.jaxis.value > -JOYSTICK_LOW) {
axis_active[0] = false;
}
}
else if (axis == JOYPAD_AXISES_Y) {
if (!axis_active[1] && event.jaxis.value > JOYSTICK_HIGH) {
axis_active[1] = true;
event.type = SDL_KEYDOWN;
event.key.keysym.scancode = SDL_SCANCODE_DOWN;
}
else if (!axis_active[1] && event.jaxis.value < -JOYSTICK_HIGH) {
axis_active[1] = true;
event.type = SDL_KEYDOWN;
event.key.keysym.scancode = SDL_SCANCODE_UP;
}
else if (axis_active[1] && event.jaxis.value < JOYSTICK_LOW && event.jaxis.value > -JOYSTICK_LOW) {
axis_active[1] = false;
}
}
}
}
}
switch (event.type) {
case SDL_QUIT: {
if (!is_running) {
exit(0);
}
else {
pending_command = GB_SDL_QUIT_COMMAND;
return;
}
}
case SDL_WINDOWEVENT: {
if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
update_viewport();
render_texture(NULL, NULL);
}
break;
}
case SDL_DROPFILE: {
if (GB_is_save_state(event.drop.file)) {
if (GB_is_inited(&gb)) {
dropped_state_file = event.drop.file;
pending_command = GB_SDL_LOAD_STATE_FROM_FILE_COMMAND;
}
else {
SDL_free(event.drop.file);
}
break;
}
else {
set_filename(event.drop.file, SDL_free);
pending_command = GB_SDL_NEW_FILE_COMMAND;
return;
}
}
case SDL_JOYBUTTONDOWN: {
if (gui_state == WAITING_FOR_JBUTTON && joypad_configuration_progress != JOYPAD_BUTTONS_MAX) {
should_render = true;
configuration.joypad_configuration[joypad_configuration_progress++] = event.jbutton.button;
}
break;
}
case SDL_JOYHATMOTION: {
if (gui_state == WAITING_FOR_JBUTTON && joypad_configuration_progress == JOYPAD_BUTTON_RIGHT) {
should_render = true;
configuration.joypad_configuration[joypad_configuration_progress++] = -1;
configuration.joypad_configuration[joypad_configuration_progress++] = -1;
configuration.joypad_configuration[joypad_configuration_progress++] = -1;
configuration.joypad_configuration[joypad_configuration_progress++] = -1;
}
break;
}
case SDL_JOYAXISMOTION: {
if (gui_state == WAITING_FOR_JBUTTON &&
joypad_configuration_progress == JOYPAD_BUTTONS_MAX &&
abs(event.jaxis.value) >= 0x4000) {
if (joypad_axis_temp == (uint8_t)-1) {
joypad_axis_temp = event.jaxis.axis;
}
else if (joypad_axis_temp != event.jaxis.axis) {
if (joypad_axis_temp < event.jaxis.axis) {
configuration.joypad_axises[JOYPAD_AXISES_X] = joypad_axis_temp;
configuration.joypad_axises[JOYPAD_AXISES_Y] = event.jaxis.axis;
}
else {
configuration.joypad_axises[JOYPAD_AXISES_Y] = joypad_axis_temp;
configuration.joypad_axises[JOYPAD_AXISES_X] = event.jaxis.axis;
}
gui_state = SHOWING_MENU;
should_render = true;
}
}
break;
}
case SDL_MOUSEWHEEL: {
if (menu_height > 144) {
scroll -= event.wheel.y;
if (scroll < 0) {
scroll = 0;
}
if (scroll >= menu_height - 144) {
scroll = menu_height - 144;
}
mouse_scroling = true;
should_render = true;
}
break;
}
case SDL_MOUSEMOTION: {
if (scrollbar_drag && scrollbar_size < 140 && scrollbar_size > 0) {
signed x = event.motion.x;
signed y = event.motion.y;
convert_mouse_coordinates(&x, &y);
signed delta = scroll_mouse_start - y;
scroll = scroll_start - delta * (signed)(menu_height - 144) / (signed)(140 - scrollbar_size);
if (scroll < 0) {
scroll = 0;
}
if (scroll >= menu_height - 144) {
scroll = menu_height - 144;
}
should_render = true;
}
break;
}
case SDL_KEYDOWN:
scrollbar_drag = false;
if (gui_state == WAITING_FOR_KEY) {
if (current_selection > 8) {
configuration.keys_2[current_selection - 9] = event.key.keysym.scancode;
}
else {
configuration.keys[current_selection] = event.key.keysym.scancode;
}
gui_state = SHOWING_MENU;
should_render = true;
}
else if (event_hotkey_code(&event) == SDL_SCANCODE_F && event.key.keysym.mod & MODIFIER) {
if ((SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN_DESKTOP) == false) {
SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP);
}
else {
SDL_SetWindowFullscreen(window, 0);
}
update_viewport();
}
else if (event_hotkey_code(&event) == SDL_SCANCODE_O) {
if (event.key.keysym.mod & MODIFIER) {
char *filename = do_open_rom_dialog();
if (filename) {
set_filename(filename, free);
pending_command = GB_SDL_NEW_FILE_COMMAND;
return;
}
}
}
else if (event.key.keysym.scancode == SDL_SCANCODE_RETURN && gui_state == WAITING_FOR_JBUTTON) {
should_render = true;
if (joypad_configuration_progress != JOYPAD_BUTTONS_MAX) {
configuration.joypad_configuration[joypad_configuration_progress] = -1;
}
else {
configuration.joypad_axises[0] = -1;
configuration.joypad_axises[1] = -1;
}
joypad_configuration_progress++;
if (joypad_configuration_progress > JOYPAD_BUTTONS_MAX) {
gui_state = SHOWING_MENU;
}
}
else if (event.key.keysym.scancode == SDL_SCANCODE_ESCAPE) {
if (gui_state == SHOWING_MENU && current_menu != root_menu) {
for (const struct menu_item *item = current_menu; item->string; item++) {
if (strcmp(item->string, "Back") == 0) {
item->handler(0);
break;
}
}
should_render = true;
}
else if (is_running) {
return;
}
else {
if (gui_state == SHOWING_DROP_MESSAGE) {
gui_state = SHOWING_MENU;
}
else if (gui_state == SHOWING_MENU) {
gui_state = SHOWING_DROP_MESSAGE;
}
current_selection = 0;
mouse_scroling = false;
scroll = 0;
current_menu = root_menu;
recalculate_menu_height();
should_render = true;
}
}
else if (gui_state == SHOWING_MENU) {
if (event.key.keysym.scancode == SDL_SCANCODE_DOWN && current_menu[current_selection + 1].string) {
current_selection++;
mouse_scroling = false;
should_render = true;
}
else if (event.key.keysym.scancode == SDL_SCANCODE_UP && current_selection) {
current_selection--;
mouse_scroling = false;
should_render = true;
}
else if (event.key.keysym.scancode == SDL_SCANCODE_RETURN && !current_menu[current_selection].backwards_handler) {
if (current_menu[current_selection].handler) {
current_menu[current_selection].handler(current_selection);
if (pending_command == GB_SDL_RESET_COMMAND && !is_running) {
pending_command = GB_SDL_NO_COMMAND;
}
if (pending_command) {
if (!is_running && pending_command == GB_SDL_QUIT_COMMAND) {
exit(0);
}
return;
}
should_render = true;
}
else {
return;
}
}
else if (event.key.keysym.scancode == SDL_SCANCODE_RIGHT && current_menu[current_selection].backwards_handler) {
current_menu[current_selection].handler(current_selection);
should_render = true;
}
else if (event.key.keysym.scancode == SDL_SCANCODE_LEFT && current_menu[current_selection].backwards_handler) {
current_menu[current_selection].backwards_handler(current_selection);
should_render = true;
}
}
else if (gui_state == SHOWING_HELP) {
gui_state = SHOWING_MENU;
should_render = true;
}
break;
}
if (should_render) {
should_render = false;
rerender:
SDL_LockSurface(converted_background);
if (width == 160 && height == 144) {
memcpy(pixels, converted_background->pixels, sizeof(pixels));
}
else {
for (unsigned y = 0; y < 144; y++) {
memcpy(pixels + x_offset + width * (y + y_offset), ((uint32_t *)converted_background->pixels) + 160 * y, 160 * 4);
}
}
SDL_UnlockSurface(converted_background);
switch (gui_state) {
case SHOWING_DROP_MESSAGE:
draw_text_centered(pixels, width, height, 8 + y_offset, "Press ESC for menu", gui_palette_native[3], gui_palette_native[0], false);
draw_text_centered(pixels, width, height, 116 + y_offset, "Drop a GB or GBC", gui_palette_native[3], gui_palette_native[0], false);
draw_text_centered(pixels, width, height, 128 + y_offset, "file to play", gui_palette_native[3], gui_palette_native[0], false);
break;
case SHOWING_MENU:
draw_text_centered(pixels, width, height, 8 + y_offset, "SameBoy", gui_palette_native[3], gui_palette_native[0], false);
unsigned i = 0, y = 24;
for (const struct menu_item *item = current_menu; item->string; item++, i++) {
if (i == current_selection && !mouse_scroling) {
if (i == 0) {
if (y < scroll) {
scroll = (y - 4) / 12 * 12;
goto rerender;
}
}
else {
if (y < scroll + 24) {
scroll = (y - 24) / 12 * 12;
goto rerender;
}
}
}
if (i == current_selection && i == 0 && scroll != 0 && !mouse_scroling) {
scroll = 0;
goto rerender;
}
if (item->value_getter && !item->backwards_handler) {
char line[25];
snprintf(line, sizeof(line), "%s%*s", item->string, 24 - (unsigned)strlen(item->string), item->value_getter(i));
draw_text_centered(pixels, width, height, y + y_offset, line, gui_palette_native[3], gui_palette_native[0],
i == current_selection ? DECORATION_SELECTION : DECORATION_NONE);
y += 12;
}
else {
draw_text_centered(pixels, width, height, y + y_offset, item->string, gui_palette_native[3], gui_palette_native[0],
i == current_selection && !item->value_getter ? DECORATION_SELECTION : DECORATION_NONE);
y += 12;
if (item->value_getter) {
draw_text_centered(pixels, width, height, y + y_offset - 1, item->value_getter(i), gui_palette_native[3], gui_palette_native[0],
i == current_selection ? DECORATION_ARROWS : DECORATION_NONE);
y += 12;
}
}
if (i == current_selection && !mouse_scroling) {
if (y > scroll + 144) {
scroll = (y - 144) / 12 * 12;
if (scroll > menu_height - 144) {
scroll = menu_height - 144;
}
goto rerender;
}
}
}
if (scrollbar_size) {
unsigned scrollbar_offset = (140 - scrollbar_size) * scroll / (menu_height - 144);
if (scrollbar_offset + scrollbar_size > 140) {
scrollbar_offset = 140 - scrollbar_size;
}
for (unsigned y = 0; y < 140; y++) {
uint32_t *pixel = pixels + x_offset + 156 + width * (y + y_offset + 2);
if (y >= scrollbar_offset && y < scrollbar_offset + scrollbar_size) {
pixel[0] = pixel[1] = gui_palette_native[2];
}
else {
pixel[0] = pixel[1] = gui_palette_native[1];
}
}
}
break;
case SHOWING_HELP:
draw_text(pixels, width, height, 2 + x_offset, 2 + y_offset, help[current_help_page], gui_palette_native[3], gui_palette_native[0], false);
break;
case WAITING_FOR_KEY:
draw_text_centered(pixels, width, height, 68 + y_offset, "Press a Key", gui_palette_native[3], gui_palette_native[0], DECORATION_NONE);
break;
case WAITING_FOR_JBUTTON:
draw_text_centered(pixels, width, height, 68 + y_offset,
joypad_configuration_progress != JOYPAD_BUTTONS_MAX ? "Press button for" : "Move the Analog Stick",
gui_palette_native[3], gui_palette_native[0], DECORATION_NONE);
draw_text_centered(pixels, width, height, 80 + y_offset,
(const char *[])
{
"Right",
"Left",
"Up",
"Down",
"A",
"B",
"Select",
"Start",
"Open Menu",
"Turbo",
"Rewind",
"Slow-Motion",
"Hotkey 1",
"Hotkey 2",
"",
} [joypad_configuration_progress],
gui_palette_native[3], gui_palette_native[0], DECORATION_NONE);
draw_text_centered(pixels, width, height, 104 + y_offset, "Press Enter to skip", gui_palette_native[3], gui_palette_native[0], DECORATION_NONE);
break;
}
render_texture(pixels, NULL);
#ifdef _WIN32
/* Required for some Windows 10 machines, god knows why */
render_texture(pixels, NULL);
#endif
}
}
}
static void __attribute__ ((constructor)) list_custom_palettes(void)
{
char *path = resource_path("Palettes");
if (!path) return;
if (strlen(path) > 1024 - 30) {
// path too long to safely concat filenames
return;
}
DIR *dir = opendir(path);
if (!dir) return;
struct dirent *ent;
while ((ent = readdir(dir))) {
unsigned length = strlen(ent->d_name);
if (length < 5 || length > 28) {
continue;
}
if (strcmp(ent->d_name + length - 4, ".sbp")) continue;
ent->d_name[length - 4] = 0;
custom_palettes = realloc(custom_palettes,
sizeof(custom_palettes[0]) * (n_custom_palettes + 1));
custom_palettes[n_custom_palettes++] = strdup(ent->d_name);
}
closedir(dir);
}