ref: 2deb15b1fc32a56de320d35d8c55cbb572376502
parent: 02c128dc98d5be47d16f74efb41e41758da1e6ea
author: Jonne Kokkonen <jonne.kokkonen@gmail.com>
date: Sat Sep 20 09:59:44 EDT 2025
Config UI refinements (#210) * add scrolling support to settings ui, add missing settings * remove mouse wheel support, little cleanups * remove some magic numbers, refactor settings state into a struct * fix binding select/back button on gamepad, use shared helpers for navigating settings * little settings ui cleanups
--- a/src/events.c
+++ b/src/events.c
@@ -85,7 +85,6 @@
// Start measuring hold time for GUIDE; trigger handled on button up after 1s hold
if (event->gbutton.button == SDL_GAMEPAD_BUTTON_BACK) {g_back_pressed_at = SDL_GetTicks();
- return ret_val;
}
if (settings_is_open()) {--- a/src/input.c
+++ b/src/input.c
@@ -316,21 +316,22 @@
void input_handle_gamepad_axis(const struct app_context *ctx, const SDL_GamepadAxis axis,
const Sint16 value) {const config_params_s *conf = &ctx->conf;
- gamepad_state.analog_values[axis] = value;
+ const Sint16 effective_value = conf->gamepad_analog_invert ? (Sint16)(-value) : value;
+ gamepad_state.analog_values[axis] = effective_value;
// Process directional axes and update button states
if (axis == conf->gamepad_analog_axis_updown) {- update_button_state_from_axis(value, conf->gamepad_analog_threshold, key_up, key_down);
+ update_button_state_from_axis(effective_value, conf->gamepad_analog_threshold, key_up, key_down);
} else if (axis == conf->gamepad_analog_axis_leftright) {- update_button_state_from_axis(value, conf->gamepad_analog_threshold, key_left, key_right);
+ update_button_state_from_axis(effective_value, conf->gamepad_analog_threshold, key_left, key_right);
} else if (axis == conf->gamepad_analog_axis_select) {- update_button_state_from_axis(value, conf->gamepad_analog_threshold, key_select, key_select);
+ update_button_state_from_axis(effective_value, conf->gamepad_analog_threshold, key_select, key_select);
} else if (axis == conf->gamepad_analog_axis_opt) {- update_button_state_from_axis(value, conf->gamepad_analog_threshold, key_opt, key_opt);
+ update_button_state_from_axis(effective_value, conf->gamepad_analog_threshold, key_opt, key_opt);
} else if (axis == conf->gamepad_analog_axis_start) {- update_button_state_from_axis(value, conf->gamepad_analog_threshold, key_start, key_start);
+ update_button_state_from_axis(effective_value, conf->gamepad_analog_threshold, key_start, key_start);
} else if (axis == conf->gamepad_analog_axis_edit) {- update_button_state_from_axis(value, conf->gamepad_analog_threshold, key_edit, key_edit);
+ update_button_state_from_axis(effective_value, conf->gamepad_analog_threshold, key_edit, key_edit);
}
keycode = gamepad_state.current_buttons;
--- a/src/settings.c
+++ b/src/settings.c
@@ -8,14 +8,14 @@
#include "fonts/fonts.h"
#include <SDL3/SDL.h>
-// Internal state
-static int g_settings_open = 0;
-static int g_selected_index = 0;
-static int g_needs_redraw = 1;
-static int g_config_saved = 0;
-static SDL_Texture *g_settings_texture = NULL;
+// Constants for the UI
+#define SETTINGS_MAX_ITEMS 64
+#define SETTINGS_MAX_LINE_LENGTH 160
+static const int SETTINGS_LINE_HEIGHT = 12;
+static const int SETTINGS_BOTTOM_PADDING = 8;
+static const int SETTINGS_MARGIN_X = 8;
+static const int SETTINGS_TITLE_SPACING = 24;
-
typedef enum capture_mode_t {CAPTURE_NONE,
CAPTURE_KEY,
@@ -23,9 +23,6 @@
CAPTURE_AXIS
} capture_mode_t;
-static capture_mode_t g_capture_mode = CAPTURE_NONE;
-static void *g_capture_target = NULL; // points to conf field
-
typedef enum item_type_t {ITEM_HEADER,
ITEM_TOGGLE_BOOL,
@@ -48,11 +45,36 @@
} setting_item_s;
typedef enum settings_view_t { VIEW_ROOT, VIEW_KEYS, VIEW_GAMEPAD, VIEW_ANALOG } settings_view_t;-static settings_view_t g_view = VIEW_ROOT;
+
+// Internal state
+typedef struct settings_ui_state {+ int is_open;
+ int selected_index;
+ int needs_redraw;
+ int config_saved;
+ SDL_Texture *texture;
+ int scroll_offset; // topmost visible item index
+ capture_mode_t capture_mode;
+ void *capture_target; // points to conf field
+ settings_view_t view;
+} settings_ui_state;
+
+static settings_ui_state g_settings = {+ .is_open = 0,
+ .selected_index = 0,
+ .needs_redraw = 1,
+ .config_saved = 0,
+ .texture = NULL,
+ .scroll_offset = 0,
+ .capture_mode = CAPTURE_NONE,
+ .capture_target = NULL,
+ .view = VIEW_ROOT,
+};
+
extern SDL_Gamepad *game_controllers[4];
-static void add_item(setting_item_s *items, int *count, const char *label, item_type_t type,
- void *target, int step, int min_val, int max_val) {+static void add_item(setting_item_s *items, int *count, const char *label, const item_type_t type,
+ void *target, const int step, const int min_val, const int max_val) { items[*count] = (setting_item_s){label, type, target, step, min_val, max_val};(*count)++;
}
@@ -59,15 +81,13 @@
static void build_menu(const config_params_s *conf, setting_item_s *items, int *count) {*count = 0;
- switch (g_view) {+ switch (g_settings.view) {case VIEW_ROOT:
add_item(items, count, "Graphics ", ITEM_HEADER, NULL, 0, 0, 0);
- add_item(items, count, "Integer scaling", ITEM_TOGGLE_BOOL, (void *)&conf->integer_scaling, 0,
- 0, 0);
+ add_item(items, count, "Integer scaling", ITEM_TOGGLE_BOOL, (void *)&conf->integer_scaling, 0,0, 0);
// SDL apps are always full screen on iOS, hide the option
if (TARGET_OS_IOS == 0) {- add_item(items, count, "Fullscreen ", ITEM_TOGGLE_BOOL, (void *)&conf->init_fullscreen, 0,
- 0, 0);
+ add_item(items, count, "Fullscreen ", ITEM_TOGGLE_BOOL, (void *)&conf->init_fullscreen, 0,0, 0);
}
add_item(items, count, "", ITEM_HEADER, NULL, 0, 0, 0);
// Audio routing does not work on iOS, hide the items when building for that
@@ -92,11 +112,19 @@
add_item(items, count, "Down ", ITEM_BIND_KEY, (void *)&conf->key_down, 0, 0, 0);
add_item(items, count, "Right ", ITEM_BIND_KEY, (void *)&conf->key_right, 0, 0, 0);
add_item(items, count, "Select ", ITEM_BIND_KEY, (void *)&conf->key_select, 0, 0, 0);
+ add_item(items, count, "Select (Alt)", ITEM_BIND_KEY, (void *)&conf->key_select_alt, 0, 0, 0);
add_item(items, count, "Start ", ITEM_BIND_KEY, (void *)&conf->key_start, 0, 0, 0);
+ add_item(items, count, "Start (Alt) ", ITEM_BIND_KEY, (void *)&conf->key_start_alt, 0, 0, 0);
add_item(items, count, "Opt ", ITEM_BIND_KEY, (void *)&conf->key_opt, 0, 0, 0);
+ add_item(items, count, "Opt (Alt) ", ITEM_BIND_KEY, (void *)&conf->key_opt_alt, 0, 0, 0);
add_item(items, count, "Edit ", ITEM_BIND_KEY, (void *)&conf->key_edit, 0, 0, 0);
+ add_item(items, count, "Edit (Alt) ", ITEM_BIND_KEY, (void *)&conf->key_edit_alt, 0, 0, 0);
add_item(items, count, "Delete ", ITEM_BIND_KEY, (void *)&conf->key_delete, 0, 0, 0);
add_item(items, count, "Reset ", ITEM_BIND_KEY, (void *)&conf->key_reset, 0, 0, 0);
+ add_item(items, count, "Jazz +Oct ", ITEM_BIND_KEY, (void *)&conf->key_jazz_inc_octave, 0, 0, 0);
+ add_item(items, count, "Jazz -Oct ", ITEM_BIND_KEY, (void *)&conf->key_jazz_dec_octave, 0, 0, 0);
+ add_item(items, count, "Jazz +Vel ", ITEM_BIND_KEY, (void *)&conf->key_jazz_inc_velocity, 0, 0, 0);
+ add_item(items, count, "Jazz -Vel ", ITEM_BIND_KEY, (void *)&conf->key_jazz_dec_velocity, 0, 0, 0);
add_item(items, count, "Toggle audio", ITEM_BIND_KEY, (void *)&conf->key_toggle_audio, 0, 0, 0);
add_item(items, count, "Toggle log ", ITEM_BIND_KEY, (void *)&conf->key_toggle_log, 0, 0, 0);
add_item(items, count, "", ITEM_HEADER, NULL, 0, 0, 0);
@@ -104,35 +132,29 @@
break;
case VIEW_GAMEPAD:
add_item(items, count, "Gamepad bindings", ITEM_HEADER, NULL, 0, 0, 0);
- add_item(items, count, "Up ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_up, 0, 0, 0);
- add_item(items, count, "Left ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_left, 0, 0, 0);
- add_item(items, count, "Down ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_down, 0, 0, 0);
- add_item(items, count, "Right ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_right, 0, 0, 0);
- add_item(items, count, "Select", ITEM_BIND_BUTTON, (void *)&conf->gamepad_select, 0, 0, 0);
- add_item(items, count, "Start ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_start, 0, 0, 0);
- add_item(items, count, "Opt ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_opt, 0, 0, 0);
- add_item(items, count, "Edit ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_edit, 0, 0, 0);
- add_item(items, count, "Quit ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_quit, 0, 0, 0);
- add_item(items, count, "Reset ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_reset, 0, 0, 0);
+ add_item(items, count, "Up ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_up, 0, 0, 0);
+ add_item(items, count, "Left ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_left, 0, 0, 0);
+ add_item(items, count, "Down ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_down, 0, 0, 0);
+ add_item(items, count, "Right ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_right, 0, 0, 0);
+ add_item(items, count, "Select ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_select, 0, 0, 0);
+ add_item(items, count, "Start ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_start, 0, 0, 0);
+ add_item(items, count, "Opt ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_opt, 0, 0, 0);
+ add_item(items, count, "Edit ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_edit, 0, 0, 0);
+ add_item(items, count, "Quit (Select +) ", ITEM_BIND_BUTTON, (void *)&conf->gamepad_quit, 0, 0, 0);
+ add_item(items, count, "Reset (Select +)", ITEM_BIND_BUTTON, (void *)&conf->gamepad_reset, 0, 0, 0);
add_item(items, count, "", ITEM_HEADER, NULL, 0, 0, 0);
add_item(items, count, "Back", ITEM_CLOSE, NULL, 0, 0, 0);
break;
case VIEW_ANALOG:
add_item(items, count, "Gamepad analog axis", ITEM_HEADER, NULL, 0, 0, 0);
- add_item(items, count, "Deadzone", ITEM_INT_ADJ, (void *)&conf->gamepad_analog_threshold, 1000,
- 1000, 32767);
- add_item(items, count, "Axis Up/Down ", ITEM_BIND_AXIS,
- (void *)&conf->gamepad_analog_axis_updown, 0, 0, 0);
- add_item(items, count, "Axis Left/Right", ITEM_BIND_AXIS,
- (void *)&conf->gamepad_analog_axis_leftright, 0, 0, 0);
- add_item(items, count, "Axis Select ", ITEM_BIND_AXIS,
- (void *)&conf->gamepad_analog_axis_select, 0, 0, 0);
- add_item(items, count, "Axis Start ", ITEM_BIND_AXIS,
- (void *)&conf->gamepad_analog_axis_start, 0, 0, 0);
- add_item(items, count, "Axis Opt ", ITEM_BIND_AXIS,
- (void *)&conf->gamepad_analog_axis_opt, 0, 0, 0);
- add_item(items, count, "Axis Edit ", ITEM_BIND_AXIS,
- (void *)&conf->gamepad_analog_axis_edit, 0, 0, 0);
+ add_item(items, count, "Invert analog ", ITEM_TOGGLE_BOOL, (void *)&conf->gamepad_analog_invert, 0,0, 0);
+ add_item(items, count, "Deadzone ", ITEM_INT_ADJ, (void *)&conf->gamepad_analog_threshold, 1000,1000, 32767);
+ add_item(items, count, "Axis Up/Down ", ITEM_BIND_AXIS, (void *)&conf->gamepad_analog_axis_updown, 0, 0, 0);
+ add_item(items, count, "Axis Left/Right", ITEM_BIND_AXIS, (void *)&conf->gamepad_analog_axis_leftright, 0, 0, 0);
+ add_item(items, count, "Axis Select ", ITEM_BIND_AXIS, (void *)&conf->gamepad_analog_axis_select, 0, 0, 0);
+ add_item(items, count, "Axis Start ", ITEM_BIND_AXIS, (void *)&conf->gamepad_analog_axis_start, 0, 0, 0);
+ add_item(items, count, "Axis Opt ", ITEM_BIND_AXIS, (void *)&conf->gamepad_analog_axis_opt, 0, 0, 0);
+ add_item(items, count, "Axis Edit ", ITEM_BIND_AXIS, (void *)&conf->gamepad_analog_axis_edit, 0, 0, 0);
add_item(items, count, "", ITEM_HEADER, NULL, 0, 0, 0);
add_item(items, count, "Back", ITEM_CLOSE, NULL, 0, 0, 0);
break;
@@ -139,33 +161,81 @@
}
}
+/**
+* @brief Compute the visible slice of the settings list and adjust scrolling.
+*
+* Ensures the selected item is visible and, when possible, pulls a preceding
+* section header into view without pushing the selection out.
+*
+* @param texture_h Render-target texture height in pixels.
+* @param list_y_top Top Y of the list in pixels (first row’s top).
+* @param total_count Total number of items (including headers).
+* @param selected_index Currently selected item index [0, total_count).
+* @param[in,out] scroll_offset_io Index of the first visible item; may be clamped/adjusted.
+* @param[out] visible_lines_out Number of list rows that fit (>= 1).
+* @param[out] start_index_out First item index to draw (inclusive).
+* @param[out] end_index_out One past the last item index to draw (exclusive).
+* @param items Items array (used to detect and include a preceding header when it fits).
+*
+* @note Line height and paddings are derived from UI constants.
+*/
+static void compute_viewport(const int texture_h, const int list_y_top, const int total_count,
+ const int selected_index,
+ int *scroll_offset_io, int *visible_lines_out, int *start_index_out,
+ int *end_index_out, const setting_item_s *items) {+ const int max_y = texture_h - SETTINGS_BOTTOM_PADDING;
+ int visible_lines = (max_y - list_y_top) / SETTINGS_LINE_HEIGHT;
+ if (visible_lines < 1)
+ visible_lines = 1;
+
+ const int max_scroll = total_count > visible_lines ? total_count - visible_lines : 0;
+ if (*scroll_offset_io > max_scroll)
+ *scroll_offset_io = max_scroll;
+ if (*scroll_offset_io < 0)
+ *scroll_offset_io = 0;
+
+ // Ensure selection visible
+ if (selected_index < *scroll_offset_io)
+ *scroll_offset_io = selected_index;
+ if (selected_index >= *scroll_offset_io + visible_lines)
+ *scroll_offset_io = selected_index - visible_lines + 1;
+
+ // If the previous item is a header, include it if it doesn't push selection out
+ if (*scroll_offset_io > 0 && items[*scroll_offset_io - 1].type == ITEM_HEADER) {+ const int candidate_start = *scroll_offset_io - 1;
+ const int candidate_end_inclusive = candidate_start + visible_lines - 1;
+ if (selected_index <= candidate_end_inclusive) {+ *scroll_offset_io = candidate_start;
+ }
+ }
+
+ const int start_i = *scroll_offset_io;
+ int end_i = total_count;
+ if (start_i + visible_lines < end_i)
+ end_i = start_i + visible_lines;
+
+ *visible_lines_out = visible_lines;
+ *start_index_out = start_i;
+ *end_index_out = end_i;
+}
+
static void settings_destroy_texture(SDL_Renderer *rend) {(void)rend;
- if (g_settings_texture != NULL) {- SDL_DestroyTexture(g_settings_texture);
- g_settings_texture = NULL;
+ if (g_settings.texture != NULL) {+ SDL_DestroyTexture(g_settings.texture);
+ g_settings.texture = NULL;
}
}
-void settings_toggle_open(void) {- g_settings_open = !g_settings_open;
- g_selected_index = 1; // first actionable item
- g_capture_mode = CAPTURE_NONE;
- g_capture_target = NULL;
- g_needs_redraw = 1;
-}
-
-bool settings_is_open(void) { return g_settings_open != 0; }-
static void settings_move(const config_params_s *conf, int delta) {- if (!g_settings_open)
+ if (!g_settings.is_open)
return;
- setting_item_s items[64];
+ setting_item_s items[SETTINGS_MAX_ITEMS];
int count = 0;
build_menu(conf, items, &count);
- int idx = g_selected_index + delta;
+ int idx = g_settings.selected_index + delta;
// Clamp within bounds
if (idx < 0)
@@ -179,7 +249,7 @@
idx += step;
}
- // Ensure we don't select before first actionable item
+ // Ensure we don't select before the first actionable item
if (idx < 1)
idx = 1;
@@ -187,14 +257,14 @@
if (idx >= count)
idx = count - 1;
- g_selected_index = idx;
- g_needs_redraw = 1;
+ g_settings.selected_index = idx;
+ g_settings.needs_redraw = 1;
}
static void settings_activate(struct app_context *ctx, const setting_item_s *items, int count) {- if (g_selected_index < 1 || g_selected_index >= count)
+ if (g_settings.selected_index < 1 || g_settings.selected_index >= count)
return;
- const setting_item_s *it = &items[g_selected_index];
+ const setting_item_s *it = &items[g_settings.selected_index];
config_params_s *conf = &ctx->conf;
switch (it->type) {@@ -210,13 +280,13 @@
if (it->target == &conf->audio_enabled && ctx->device_connected) {audio_toggle(ctx->conf.audio_device_name, ctx->conf.audio_buffer_size);
}
- g_needs_redraw = 1;
+ g_settings.needs_redraw = 1;
break;
}
case ITEM_SAVE: {write_config(conf);
- g_needs_redraw = 1;
- g_config_saved = 1;
+ g_settings.needs_redraw = 1;
+ g_settings.config_saved = 1;
break;
}
case ITEM_CLOSE:
@@ -223,58 +293,139 @@
settings_toggle_open();
break;
case ITEM_BIND_KEY:
- g_capture_mode = CAPTURE_KEY;
- g_capture_target = it->target;
- g_needs_redraw = 1;
+ g_settings.capture_mode = CAPTURE_KEY;
+ g_settings.capture_target = it->target;
+ g_settings.needs_redraw = 1;
break;
case ITEM_BIND_BUTTON:
- g_capture_mode = CAPTURE_BUTTON;
- g_capture_target = it->target;
- g_needs_redraw = 1;
+ g_settings.capture_mode = CAPTURE_BUTTON;
+ g_settings.capture_target = it->target;
+ g_settings.needs_redraw = 1;
break;
case ITEM_BIND_AXIS:
- g_capture_mode = CAPTURE_AXIS;
- g_capture_target = it->target;
- g_needs_redraw = 1;
+ g_settings.capture_mode = CAPTURE_AXIS;
+ g_settings.capture_target = it->target;
+ g_settings.needs_redraw = 1;
break;
- case ITEM_INT_ADJ:
- case ITEM_HEADER:
- break;
default:
break;
}
}
+// Shared helpers for handling back/cancel, adjust, and enter/select
+static void settings_handle_back(void) {+ if (g_settings.capture_mode != CAPTURE_NONE) {+ g_settings.capture_mode = CAPTURE_NONE;
+ g_settings.capture_target = NULL;
+ g_settings.needs_redraw = 1;
+ return;
+ }
+ if (g_settings.view != VIEW_ROOT) {+ g_settings.view = VIEW_ROOT;
+ g_settings.selected_index = 1;
+ g_settings.needs_redraw = 1;
+ return;
+ }
+ settings_toggle_open();
+}
+
+static int settings_adjust_selected(const config_params_s *conf, int direction) {+ setting_item_s items[SETTINGS_MAX_ITEMS];
+ int count = 0;
+ build_menu(conf, items, &count);
+ if (g_settings.selected_index > 0 && g_settings.selected_index < count) {+ setting_item_s *it = &items[g_settings.selected_index];
+ if (it->type == ITEM_INT_ADJ && it->target != NULL) {+ int *val = it->target;
+ const int delta = (direction < 0 ? -it->step : it->step);
+ int new_val = *val + delta;
+ if (new_val < it->min_val)
+ new_val = it->min_val;
+ if (new_val > it->max_val)
+ new_val = it->max_val;
+ *val = new_val;
+ g_settings.needs_redraw = 1;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static void settings_handle_enter(struct app_context *ctx) {+ setting_item_s items[SETTINGS_MAX_ITEMS];
+ int count = 0;
+ build_menu(&ctx->conf, items, &count);
+ // Handle entering submenus from root based on the item type
+ if (g_settings.view == VIEW_ROOT) {+ const setting_item_s *it = &items[g_settings.selected_index];
+ if (it->type == ITEM_SUBMENU && it->label &&
+ SDL_strstr(it->label, "Keyboard bindings") == it->label) {+ g_settings.view = VIEW_KEYS;
+ g_settings.selected_index = 1;
+ g_settings.scroll_offset = 0;
+ g_settings.needs_redraw = 1;
+ return;
+ }
+ if (it->type == ITEM_SUBMENU && it->label &&
+ SDL_strstr(it->label, "Gamepad bindings") == it->label) {+ g_settings.view = VIEW_GAMEPAD;
+ g_settings.selected_index = 1;
+ g_settings.scroll_offset = 0;
+ g_settings.needs_redraw = 1;
+ return;
+ }
+ if (it->type == ITEM_SUBMENU && it->label &&
+ SDL_strstr(it->label, "Gamepad analog axis") == it->label) {+ g_settings.view = VIEW_ANALOG;
+ g_settings.selected_index = 1;
+ g_settings.scroll_offset = 0;
+ g_settings.needs_redraw = 1;
+ return;
+ }
+ }
+ // Back item in submenus
+ if (g_settings.view != VIEW_ROOT) {+ const setting_item_s *it = &items[g_settings.selected_index];
+ if (it->type == ITEM_CLOSE && it->label && SDL_strcmp(it->label, "Back") == 0) {+ g_settings.view = VIEW_ROOT;
+ g_settings.selected_index = 1;
+ g_settings.scroll_offset = 0;
+ g_settings.needs_redraw = 1;
+ return;
+ }
+ }
+ settings_activate(ctx, items, count);
+}
+
+void settings_toggle_open(void) {+ g_settings.is_open = !g_settings.is_open;
+ g_settings.selected_index = 1; // first actionable item
+ g_settings.capture_mode = CAPTURE_NONE;
+ g_settings.capture_target = NULL;
+ g_settings.scroll_offset = 0;
+ g_settings.needs_redraw = 1;
+}
+
+bool settings_is_open(void) { return g_settings.is_open != 0; }+
void settings_handle_event(struct app_context *ctx, const SDL_Event *e) {- if (!g_settings_open)
+ if (!g_settings.is_open)
return;
if (e->type == SDL_EVENT_KEY_DOWN) { if (e->key.key == SDLK_ESCAPE || e->key.key == SDLK_F1) {- if (g_capture_mode != CAPTURE_NONE) {- g_capture_mode = CAPTURE_NONE;
- g_capture_target = NULL;
- g_needs_redraw = 1;
- return;
- }
- if (g_view != VIEW_ROOT) {- g_view = VIEW_ROOT;
- g_selected_index = 1;
- g_needs_redraw = 1;
- return;
- }
- settings_toggle_open();
+ settings_handle_back();
return;
}
// Capture key remap
- if (g_capture_mode == CAPTURE_KEY) {- if (g_capture_target != NULL) {- unsigned int *dst = g_capture_target;
+ if (g_settings.capture_mode == CAPTURE_KEY) {+ if (g_settings.capture_target != NULL) {+ unsigned int *dst = g_settings.capture_target;
*dst = e->key.scancode;
}
- g_capture_mode = CAPTURE_NONE;
- g_capture_target = NULL;
- g_needs_redraw = 1;
+ g_settings.capture_mode = CAPTURE_NONE;
+ g_settings.capture_target = NULL;
+ g_settings.needs_redraw = 1;
return;
}
if (e->key.key == SDLK_UP) {@@ -286,65 +437,11 @@
return;
}
if (e->key.key == SDLK_LEFT || e->key.key == SDLK_RIGHT) {- setting_item_s items[64];
- int count = 0;
- build_menu(&ctx->conf, items, &count);
- if (g_selected_index > 0 && g_selected_index < count) {- setting_item_s *it = &items[g_selected_index];
- if (it->type == ITEM_INT_ADJ && it->target != NULL) {- int *val = (int *)it->target;
- int delta = (e->key.key == SDLK_LEFT ? -it->step : it->step);
- int newv = *val + delta;
- if (newv < it->min_val)
- newv = it->min_val;
- if (newv > it->max_val)
- newv = it->max_val;
- *val = newv;
- g_needs_redraw = 1;
- return;
- }
- }
+ if (settings_adjust_selected(&ctx->conf, e->key.key == SDLK_LEFT ? -1 : 1))
+ return;
}
if (e->key.key == SDLK_RETURN || e->key.key == SDLK_SPACE) {- setting_item_s items[64];
- int count = 0;
- build_menu(&ctx->conf, items, &count);
- // Handle entering submenus from root based on item type
- if (g_view == VIEW_ROOT) {- const setting_item_s *it = &items[g_selected_index];
- if (it->type == ITEM_SUBMENU && it->label &&
- SDL_strstr(it->label, "Keyboard bindings") == it->label) {- g_view = VIEW_KEYS;
- g_selected_index = 1;
- g_needs_redraw = 1;
- return;
- }
- if (it->type == ITEM_SUBMENU && it->label &&
- SDL_strstr(it->label, "Gamepad bindings") == it->label) {- g_view = VIEW_GAMEPAD;
- g_selected_index = 1;
- g_needs_redraw = 1;
- return;
- }
- if (it->type == ITEM_SUBMENU && it->label &&
- SDL_strstr(it->label, "Gamepad analog axis") == it->label) {- g_view = VIEW_ANALOG;
- g_selected_index = 1;
- g_needs_redraw = 1;
- return;
- }
- }
- // Back item in submenus
- if (g_view != VIEW_ROOT) {- const setting_item_s *it = &items[g_selected_index];
- if (it->type == ITEM_CLOSE && it->label && SDL_strcmp(it->label, "Back") == 0) {- g_view = VIEW_ROOT;
- g_selected_index = 1;
- g_needs_redraw = 1;
- return;
- }
- }
- settings_activate(ctx, items, count);
+ settings_handle_enter(ctx);
return;
}
}
@@ -355,24 +452,12 @@
// Cancel capture or go back/close with B/Back
if (btn == SDL_GAMEPAD_BUTTON_EAST || btn == SDL_GAMEPAD_BUTTON_BACK) {- if (g_capture_mode != CAPTURE_NONE) {- g_capture_mode = CAPTURE_NONE;
- g_capture_target = NULL;
- g_needs_redraw = 1;
- return;
- }
- if (g_view != VIEW_ROOT) {- g_view = VIEW_ROOT;
- g_selected_index = 1;
- g_needs_redraw = 1;
- return;
- }
- settings_toggle_open();
+ settings_handle_back();
return;
}
// If capturing a button, let the capture handler below process it
- if (g_capture_mode == CAPTURE_NONE) {+ if (g_settings.capture_mode == CAPTURE_NONE) {// D-pad navigation
if (btn == SDL_GAMEPAD_BUTTON_DPAD_UP) {settings_move(&ctx->conf, -1);
@@ -383,66 +468,12 @@
return;
}
if (btn == SDL_GAMEPAD_BUTTON_DPAD_LEFT || btn == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {- setting_item_s items[64];
- int count = 0;
- build_menu(&ctx->conf, items, &count);
- if (g_selected_index > 0 && g_selected_index < count) {- setting_item_s *it = &items[g_selected_index];
- if (it->type == ITEM_INT_ADJ && it->target != NULL) {- int *val = (int *)it->target;
- int delta = (btn == SDL_GAMEPAD_BUTTON_DPAD_LEFT ? -it->step : it->step);
- int newv = *val + delta;
- if (newv < it->min_val)
- newv = it->min_val;
- if (newv > it->max_val)
- newv = it->max_val;
- *val = newv;
- g_needs_redraw = 1;
- return;
- }
- }
+ if (settings_adjust_selected(&ctx->conf, btn == SDL_GAMEPAD_BUTTON_DPAD_LEFT ? -1 : 1))
+ return;
}
// Activate/select with A
if (btn == SDL_GAMEPAD_BUTTON_SOUTH) {- setting_item_s items[64];
- int count = 0;
- build_menu(&ctx->conf, items, &count);
- // Handle entering submenus from root based on item type
- if (g_view == VIEW_ROOT) {- const setting_item_s *it = &items[g_selected_index];
- if (it->type == ITEM_SUBMENU && it->label &&
- SDL_strstr(it->label, "Keyboard bindings") == it->label) {- g_view = VIEW_KEYS;
- g_selected_index = 1;
- g_needs_redraw = 1;
- return;
- }
- if (it->type == ITEM_SUBMENU && it->label &&
- SDL_strstr(it->label, "Gamepad bindings") == it->label) {- g_view = VIEW_GAMEPAD;
- g_selected_index = 1;
- g_needs_redraw = 1;
- return;
- }
- if (it->type == ITEM_SUBMENU && it->label &&
- SDL_strstr(it->label, "Gamepad analog axis") == it->label) {- g_view = VIEW_ANALOG;
- g_selected_index = 1;
- g_needs_redraw = 1;
- return;
- }
- }
- // Back item in submenus
- if (g_view != VIEW_ROOT) {- const setting_item_s *it = &items[g_selected_index];
- if (it->type == ITEM_CLOSE && it->label && SDL_strcmp(it->label, "Back") == 0) {- g_view = VIEW_ROOT;
- g_selected_index = 1;
- g_needs_redraw = 1;
- return;
- }
- }
- settings_activate(ctx, items, count);
+ settings_handle_enter(ctx);
return;
}
}
@@ -449,26 +480,26 @@
}
// Capture gamepad button
- if (g_capture_mode == CAPTURE_BUTTON && e->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {- if (g_capture_target != NULL) {- int *dst = (int *)g_capture_target;
+ if (g_settings.capture_mode == CAPTURE_BUTTON && e->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {+ if (g_settings.capture_target != NULL) {+ int *dst = (int *)g_settings.capture_target;
*dst = e->gbutton.button;
}
- g_capture_mode = CAPTURE_NONE;
- g_capture_target = NULL;
- g_needs_redraw = 1;
+ g_settings.capture_mode = CAPTURE_NONE;
+ g_settings.capture_target = NULL;
+ g_settings.needs_redraw = 1;
return;
}
// Capture axis on significant motion
- if (g_capture_mode == CAPTURE_AXIS && e->type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {+ if (g_settings.capture_mode == CAPTURE_AXIS && e->type == SDL_EVENT_GAMEPAD_AXIS_MOTION) { if (SDL_abs(e->gaxis.value) > 16000) {- if (g_capture_target != NULL) {- int *dst = (int *)g_capture_target;
+ if (g_settings.capture_target != NULL) {+ int *dst = (int *)g_settings.capture_target;
*dst = e->gaxis.axis;
}
- g_capture_mode = CAPTURE_NONE;
- g_capture_target = NULL;
- g_needs_redraw = 1;
+ g_settings.capture_mode = CAPTURE_NONE;
+ g_settings.capture_target = NULL;
+ g_settings.needs_redraw = 1;
return;
}
}
@@ -476,7 +507,7 @@
void settings_render_overlay(SDL_Renderer *rend, const config_params_s *conf, int texture_w,
int texture_h) {- if (!g_settings_open)
+ if (!g_settings.is_open)
return;
const struct inline_font *previous_font = inline_font_get_current();
@@ -486,25 +517,25 @@
inline_font_initialize(fonts_get(0));
}
- if (g_settings_texture == NULL) {- g_settings_texture = SDL_CreateTexture(rend, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET,
- texture_w, texture_h);
- if (g_settings_texture == NULL) {+ if (g_settings.texture == NULL) {+ g_settings.texture = SDL_CreateTexture(rend, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET,
+ texture_w, texture_h);
+ if (g_settings.texture == NULL) {inline_font_close();
inline_font_initialize(previous_font);
return;
}
- SDL_SetTextureBlendMode(g_settings_texture, SDL_BLENDMODE_BLEND);
- SDL_SetTextureScaleMode(g_settings_texture, SDL_SCALEMODE_NEAREST);
- g_needs_redraw = 1;
+ SDL_SetTextureBlendMode(g_settings.texture, SDL_BLENDMODE_BLEND);
+ SDL_SetTextureScaleMode(g_settings.texture, SDL_SCALEMODE_NEAREST);
+ g_settings.needs_redraw = 1;
}
- if (!g_needs_redraw)
+ if (!g_settings.needs_redraw)
goto composite;
- g_needs_redraw = 0;
+ g_settings.needs_redraw = 0;
SDL_Texture *prev = SDL_GetRenderTarget(rend);
- SDL_SetRenderTarget(rend, g_settings_texture);
+ SDL_SetRenderTarget(rend, g_settings.texture);
// Background
SDL_SetRenderDrawColor(rend, 0, 0, 0, 240);
@@ -519,47 +550,74 @@
const Uint32 section_header = 0xAAAAFF;
const int margin_x_unselected = fonts_get(0)->glyph_x+1;
const int margin_x_selected = fonts_get(0)->glyph_x+1;
- int x = 8;
+ int x = SETTINGS_MARGIN_X;
int y = 8;
inprint(rend, "M8C Config", x, y, title, title);
- y += 24;
+ y += SETTINGS_TITLE_SPACING;
- setting_item_s items[64];
+ setting_item_s items[SETTINGS_MAX_ITEMS];
int count = 0;
build_menu(conf, items, &count);
- if (g_selected_index >= count)
- g_selected_index = count - 1;
+ if (g_settings.selected_index >= count)
+ g_settings.selected_index = count - 1;
- if (g_capture_mode == CAPTURE_KEY) {+ if (g_settings.capture_mode == CAPTURE_KEY) {inprint(rend, "Press a key... (Esc to cancel)", x, y, selected_item_fg, selected_item_fg);
- } else if (g_capture_mode == CAPTURE_BUTTON) {+ } else if (g_settings.capture_mode == CAPTURE_BUTTON) {inprint(rend, "Press a gamepad button...", x, y, selected_item_fg, selected_item_fg);
- } else if (g_capture_mode == CAPTURE_AXIS) {+ } else if (g_settings.capture_mode == CAPTURE_AXIS) {inprint(rend, "Move a gamepad axis...", x, y, selected_item_fg, selected_item_fg);
}
- if (g_config_saved) {+ if (g_settings.config_saved) {inprint(rend, "Configuration saved", x, y, selected_item_fg, selected_item_fg);
- g_config_saved = 0;
+ g_settings.config_saved = 0;
}
- y += 12;
+ y += SETTINGS_LINE_HEIGHT;
+ // Compute viewport and clamp scroll
+ const int list_y_top = y;
+ int visible_lines = 0;
+ int start_i = 0;
+ int end_i = 0;
+ compute_viewport(texture_h, list_y_top, count, g_settings.selected_index, &g_settings.scroll_offset,
+ &visible_lines, &start_i, &end_i, items);
- for (int i = 0; i < count; i++) {- const int selected = (i == g_selected_index);
+ // Draw scroll indicators when more content exists above/below
+ if (visible_lines > 0) {+ const int arrow_x = texture_w - SETTINGS_MARGIN_X - (int)fonts_get(0)->glyph_x;
+ // Show up-arrow only when there are selectable items above
+ int has_above = g_settings.scroll_offset > 0;
+ if (has_above && items[g_settings.scroll_offset - 1].type == ITEM_HEADER && g_settings.scroll_offset == 1) {+ has_above = 0;
+ }
+ if (has_above) {+ inprint(rend, "+", arrow_x, list_y_top, selected_item_fg, selected_item_bg);
+ }
+ if (g_settings.scroll_offset + visible_lines < count) {+ const int max_y = texture_h - SETTINGS_BOTTOM_PADDING;
+ int bottom_arrow_y = list_y_top + (visible_lines - 1) * SETTINGS_LINE_HEIGHT;
+ if (bottom_arrow_y > max_y - SETTINGS_LINE_HEIGHT)
+ bottom_arrow_y = max_y - SETTINGS_LINE_HEIGHT;
+ inprint(rend, "+", arrow_x, bottom_arrow_y, selected_item_fg, selected_item_bg);
+ }
+ }
+
+ for (int i = start_i; i < end_i; i++) {+ const int selected = (i == g_settings.selected_index);
const setting_item_s *it = &items[i];
if (it->type == ITEM_HEADER) {inprint(rend, it->label, x, y, section_header, section_header);
- y += 12;
+ y += SETTINGS_LINE_HEIGHT;
continue;
}
if (it->type == ITEM_TOGGLE_BOOL) {const unsigned int *val = (const unsigned int *)it->target;
- char line[160];
- SDL_snprintf(line, sizeof line, "%s [%s]", it->label, (*val ? "On" : "Off"));
+ char line[SETTINGS_MAX_LINE_LENGTH];
+ SDL_snprintf(line, SETTINGS_MAX_LINE_LENGTH, "%s [%s]", it->label, (*val ? "On" : "Off"));
inprint(rend, line, x + (selected ? margin_x_selected : margin_x_unselected), y,
selected ? selected_item_fg : fg, selected ? selected_item_bg : bg);
- y += 12;
+ y += SETTINGS_LINE_HEIGHT;
continue;
}
if (it->type == ITEM_BIND_KEY) {@@ -568,11 +626,11 @@
if (name == NULL || name[0] == '\0') {name = "Unknown";
}
- char line[160];
- SDL_snprintf(line, sizeof line, "%s: %s", it->label, name);
+ char line[SETTINGS_MAX_LINE_LENGTH];
+ SDL_snprintf(line, SETTINGS_MAX_LINE_LENGTH, "%s: %s", it->label, name);
inprint(rend, line, x + (selected ? margin_x_selected : margin_x_unselected), y,
selected ? selected_item_fg : fg, selected ? selected_item_bg : bg);
- y += 12;
+ y += SETTINGS_LINE_HEIGHT;
continue;
}
if (it->type == ITEM_BIND_BUTTON) {@@ -600,15 +658,15 @@
name = SDL_GetGamepadStringForButton((SDL_GamepadButton)v);
}
- char line[160];
+ char line[SETTINGS_MAX_LINE_LENGTH];
if (name && name[0] != '\0') {- SDL_snprintf(line, sizeof line, "%s: %s", it->label, name);
+ SDL_snprintf(line, SETTINGS_MAX_LINE_LENGTH, "%s: %s", it->label, name);
} else {- SDL_snprintf(line, sizeof line, "%s: %d", it->label, v);
+ SDL_snprintf(line, SETTINGS_MAX_LINE_LENGTH, "%s: %d", it->label, v);
}
inprint(rend, line, x + (selected ? margin_x_selected : margin_x_unselected), y,
selected ? selected_item_fg : fg, selected ? selected_item_bg : bg);
- y += 12;
+ y += SETTINGS_LINE_HEIGHT;
continue;
}
if (it->type == ITEM_BIND_AXIS) {@@ -615,39 +673,39 @@
int v = *(int *)it->target;
const char *name = SDL_GetGamepadStringForAxis((SDL_GamepadAxis)v);
- char line[160];
+ char line[SETTINGS_MAX_LINE_LENGTH];
if (name && name[0] != '\0') {- SDL_snprintf(line, sizeof line, "%s: %s", it->label, name);
+ SDL_snprintf(line, SETTINGS_MAX_LINE_LENGTH, "%s: %s", it->label, name);
} else {- SDL_snprintf(line, sizeof line, "%s: %d", it->label, v);
+ SDL_snprintf(line, SETTINGS_MAX_LINE_LENGTH, "%s: %d", it->label, v);
}
inprint(rend, line, x + (selected ? margin_x_selected : margin_x_unselected), y,
selected ? selected_item_fg : fg, selected ? selected_item_bg : bg);
- y += 12;
+ y += SETTINGS_LINE_HEIGHT;
continue;
}
if (it->type == ITEM_INT_ADJ) {- char line[160];
- SDL_snprintf(line, sizeof line, "%s: %d (Left/Right)", it->label, *(int *)it->target);
+ char line[SETTINGS_MAX_LINE_LENGTH];
+ SDL_snprintf(line, SETTINGS_MAX_LINE_LENGTH, "%s: %d (Left/Right)", it->label, *(int *)it->target);
inprint(rend, line, x + (selected ? margin_x_selected : margin_x_unselected), y,
selected ? selected_item_fg : fg, selected ? selected_item_bg : bg);
- y += 12;
+ y += SETTINGS_LINE_HEIGHT;
continue;
}
if (it->type == ITEM_SAVE || it->type == ITEM_CLOSE) {- char line[160];
- SDL_snprintf(line, sizeof line, "%s", it->label);
+ char line[SETTINGS_MAX_LINE_LENGTH];
+ SDL_snprintf(line, SETTINGS_MAX_LINE_LENGTH, "%s", it->label);
inprint(rend, line, x + (selected ? margin_x_selected : margin_x_unselected), y,
selected ? selected_item_fg : fg, selected ? selected_item_bg : bg);
- y += 12;
+ y += SETTINGS_LINE_HEIGHT;
continue;
}
if (it->type == ITEM_SUBMENU) {- char line[160];
- SDL_snprintf(line, sizeof line, "%s", it->label);
+ char line[SETTINGS_MAX_LINE_LENGTH];
+ SDL_snprintf(line, SETTINGS_MAX_LINE_LENGTH, "%s", it->label);
inprint(rend, line, x + (selected ? margin_x_selected : margin_x_unselected), y,
selected ? selected_item_fg : fg, selected ? selected_item_bg : bg);
- y += 12;
+ y += SETTINGS_LINE_HEIGHT;
}
}
@@ -654,7 +712,7 @@
SDL_SetRenderTarget(rend, prev);
composite:
- SDL_RenderTexture(rend, g_settings_texture, NULL, NULL);
+ SDL_RenderTexture(rend, g_settings.texture, NULL, NULL);
if (previous_font->glyph_x != fonts_get(0)->glyph_x) {inline_font_close();
inline_font_initialize(previous_font);
@@ -664,5 +722,6 @@
// Handle renderer/size resets: drop the cached texture to recreate at new size on next frame
void settings_on_texture_size_change(SDL_Renderer *rend) {settings_destroy_texture(rend);
- g_needs_redraw = 1;
+ g_settings.scroll_offset = 0;
+ g_settings.needs_redraw = 1;
}
--- a/src/settings.h
+++ b/src/settings.h
@@ -25,11 +25,6 @@
// Notify settings overlay that logical render size changed; drops cached texture
void settings_on_texture_size_change(SDL_Renderer *rend);
-// Set the font (SDL_Texture*) to be used while rendering the settings menu.
-// Pass NULL to use the current global font.
-void settings_set_font(SDL_Texture *font);
-
-
#endif // SETTINGS_H_
--
⑨