SDL_Libretro 0.1.0
SDL3-power libretro frontend.
Loading...
Searching...
No Matches
SDL_libretro_menu.h
Go to the documentation of this file.
1
24#if defined(SDL_LIBRETRO_IMPLEMENTATION) && !defined(SDL_LIBRETRO_MENU_IMPL_ONCE) && defined(SDL_LIBRETRO_ENABLE_MENU) && !defined(SDL_LIBRETRO_DISABLE_MENU)
25#define SDL_LIBRETRO_MENU_IMPL_ONCE
26
27// Nuklear, with the feature set the SDL3 renderer backend and nuklear_console
28// require. Each define is guarded so an application can pre-configure Nuklear.
29#ifndef NK_INCLUDE_FIXED_TYPES
30#define NK_INCLUDE_FIXED_TYPES
31#endif
32#ifndef NK_INCLUDE_STANDARD_VARARGS
33#define NK_INCLUDE_STANDARD_VARARGS
34#endif
35#ifndef NK_INCLUDE_DEFAULT_ALLOCATOR
36#define NK_INCLUDE_DEFAULT_ALLOCATOR
37#endif
38#ifndef NK_INCLUDE_STANDARD_IO
39#define NK_INCLUDE_STANDARD_IO
40#endif
41#ifndef NK_INCLUDE_FONT_BAKING
42#define NK_INCLUDE_FONT_BAKING
43#endif
44#ifndef NK_INCLUDE_DEFAULT_FONT
45#define NK_INCLUDE_DEFAULT_FONT
46#endif
47#ifndef NK_INCLUDE_COMMAND_USERDATA
48#define NK_INCLUDE_COMMAND_USERDATA
49#endif
50#ifndef NK_INCLUDE_VERTEX_BUFFER_OUTPUT
51#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
52#endif
53#ifndef NK_BUTTON_TRIGGER_ON_RELEASE
54#define NK_BUTTON_TRIGGER_ON_RELEASE
55#endif
56
57#ifndef SDL_LIBRETRO_MENU_NO_NK_IMPLEMENTATION
58#define NK_IMPLEMENTATION
59#endif
60#ifndef SDL_LIBRETRO_MENU_NUKLEAR_H
61#define SDL_LIBRETRO_MENU_NUKLEAR_H "nuklear.h"
62#endif
63#include SDL_LIBRETRO_MENU_NUKLEAR_H
64
65#ifndef SDL_LIBRETRO_MENU_NO_NK_IMPLEMENTATION
66#define NK_SDL3_RENDERER_IMPLEMENTATION
67#endif
68#ifndef SDL_LIBRETRO_MENU_NUKLEAR_SDL3_RENDERER_H
69#define SDL_LIBRETRO_MENU_NUKLEAR_SDL3_RENDERER_H "demo/sdl3_renderer/nuklear_sdl3_renderer.h"
70#endif
71#include SDL_LIBRETRO_MENU_NUKLEAR_SDL3_RENDERER_H
72
73#ifndef SDL_LIBRETRO_MENU_NO_NK_IMPLEMENTATION
74#define NK_GAMEPAD_IMPLEMENTATION
75#endif
76#ifndef SDL_LIBRETRO_MENU_NUKLEAR_GAMEPAD_H
77#define SDL_LIBRETRO_MENU_NUKLEAR_GAMEPAD_H "nuklear_gamepad.h"
78#endif
79#include SDL_LIBRETRO_MENU_NUKLEAR_GAMEPAD_H
80
81// The file browser enumerates directories through nuklear_console's SDL3
82// backend, selected automatically since SDL is included first.
83#ifndef SDL_LIBRETRO_MENU_NO_NK_IMPLEMENTATION
84#define NK_CONSOLE_IMPLEMENTATION
85#endif
86// Load Game opens the OS file dialog, which nuklear_console enables with SDL3.
87// It falls back to its own file browser when a gamepad activates Load Game,
88// or the dialog can't open. Define NK_CONSOLE_FILE_NO_SDL_NATIVE_DIALOG to
89// always use the file browser.
90#ifndef SDL_LIBRETRO_MENU_NUKLEAR_CONSOLE_H
91#define SDL_LIBRETRO_MENU_NUKLEAR_CONSOLE_H "nuklear_console.h"
92#endif
93#include SDL_LIBRETRO_MENU_NUKLEAR_CONSOLE_H
94#include "nuklear_console_sdl.h"
95
96#ifdef __EMSCRIPTEN__
97#include <emscripten.h>
98#endif
99
100#ifndef SDL_LIBRETRO_MENU_TOGGLE_KEY
106#define SDL_LIBRETRO_MENU_TOGGLE_KEY SDLK_F1
107#endif
108
109#ifndef SDL_LIBRETRO_MENU_FONT_HEIGHT
116#define SDL_LIBRETRO_MENU_FONT_HEIGHT 13
117#endif
118
119#ifndef SDL_LIBRETRO_MENU_DEFAULT_STYLE
125#define SDL_LIBRETRO_MENU_DEFAULT_STYLE SDL_LIBRETRO_MENU_STYLE_CATPPUCCIN_MOCHA
126#endif
127
132#define SDL_LIBRETRO_MENU_STYLE_NAMES "Mocha|Latte|Frappe|Macchiato|Dracula|Dark"
133
139#define SDL_LIBRETRO_MENU_LANGUAGE_NAMES \
140 "Auto|English|Japanese|French|Spanish|German|Italian|Dutch|" \
141 "Portuguese (Brazil)|Portuguese (Portugal)|Russian|Korean|" \
142 "Chinese (Traditional)|Chinese (Simplified)|Esperanto|Polish|Vietnamese|Arabic|" \
143 "Greek|Turkish|Slovak|Persian|Hebrew|Asturian|Finnish|Indonesian|Swedish|" \
144 "Ukrainian|Czech|Catalan (Valencia)|Catalan|British English|Hungarian|" \
145 "Belarusian|Galician|Norwegian|Irish|Thai"
146
154typedef struct SDL_LibretroMenuOptionState {
155 SDL_LibretroMenu* menu;
156 unsigned index;
157 int selected;
158 nk_bool checked;
159 char* displayList;
160} SDL_LibretroMenuOptionState;
161
168typedef struct SDL_LibretroMenuPortState {
169 SDL_LibretroMenu* menu;
170 unsigned port;
171 int selected;
172 char* deviceList;
173 char label[16];
174} SDL_LibretroMenuPortState;
175
180typedef struct SDL_LibretroMenuKeyBind {
181 SDL_LibretroMenu* menu;
182 int button;
183 nk_rune rune;
184} SDL_LibretroMenuKeyBind;
185
190typedef struct SDL_LibretroMenuPadBind {
191 SDL_LibretroMenu* menu;
192 unsigned port;
193 int button;
194 enum nk_gamepad_button captured;
195} SDL_LibretroMenuPadBind;
196
201typedef struct SDL_LibretroMenuPadPort {
202 SDL_LibretroMenu* menu;
203 unsigned port;
204 int analogIndex;
205 char label[64];
206 SDL_LibretroMenuPadBind binds[SDL_LIBRETRO_MAX_JOYPAD_BUTTONS];
207} SDL_LibretroMenuPadPort;
208
214struct SDL_LibretroMenu {
215 SDL_Libretro* lr;
216 struct nk_context* ctx;
217 nk_console* console;
218 struct nk_gamepads gamepads;
219 struct nk_font_atlas* atlas;
220 float renderScale;
221 void* userData;
223 bool open;
224 bool wasOpen;
225 bool keysHeldAtOpen[SDL_SCANCODE_COUNT];
226 unsigned int padButtonsHeldAtOpen[NK_GAMEPAD_MAX];
227 bool uiBuilt;
229 // Widgets whose visibility depends on the running game.
230 nk_console* resumeButton;
231 nk_console* stateRow;
232 nk_console* screenshotButton;
233 nk_console* resetButton;
234 nk_console* optionsButton;
235 nk_console* controllersButton;
236 nk_console* loadGameButton;
237
238 // Settings state the widgets write into.
239 int volumePercent;
240 int fitModeIndex;
241 int styleIndex;
242 int uiScaleIndex;
244 int filterIndex;
245 nk_bool fullscreenChecked;
246 nk_bool vsyncChecked;
247 nk_bool muteChecked;
248 nk_bool rewindChecked;
249 int rewindBufferMB;
250 int mousePortIndex;
251 char mousePortList[16 * SDL_LIBRETRO_MAX_USERS];
252 int languageIndex;
253 float preMuteVolume;
255 // Load Game
256 char loadGamePath[SDL_LIBRETRO_MAX_PATH];
257
258 // Core Options rebuild tracking
259 SDL_LibretroMenuOptionState* optionStates;
260 unsigned optionStateCount;
261 bool optionsStale;
262 bool settingsDirty;
263 unsigned builtOptionCount;
264 unsigned builtCoreGeneration;
266 // Controllers
267 SDL_LibretroMenuPortState portStates[SDL_LIBRETRO_MAX_USERS];
268
269 // Keyboard bindings for player 1 (Settings > Keyboard).
270 SDL_LibretroMenuKeyBind keyBinds[SDL_LIBRETRO_MAX_JOYPAD_BUTTONS];
271
272 // Per-player gamepad bindings and analog-to-digital (Settings > Gamepad),
273 // rebuilt when the menu opens so the gamepad names stay current.
274 nk_console* gamepadButton;
275 SDL_LibretroMenuPadPort padPorts[SDL_LIBRETRO_MAX_USERS];
276
277 // Disks, rebuilt when the menu opens.
278 nk_console* disksButton;
279 int diskSelected;
280 unsigned builtDiskCount;
281 char** diskLabels;
283 // Progress rows, refreshed every update.
284 nk_console* osdProgressWidget;
285 nk_size osdProgressValue;
286 char osdProgressLabel[128];
287
288 nk_console* quitButton;
290 // Settings textedit buffers (directories + username).
291 char coreDirBuffer[SDL_LIBRETRO_MAX_PATH];
292 char saveDirBuffer[SDL_LIBRETRO_MAX_PATH];
293 char systemDirBuffer[SDL_LIBRETRO_MAX_PATH];
294 char browseDirBuffer[SDL_LIBRETRO_MAX_PATH];
295 char usernameBuffer[64];
296
297 // About page, rebuilt each time it opens. The labels read the arena
298 // lazily, so it must stay allocated for as long as the menu.
299 nk_console* aboutButton;
300 char aboutText[4096];
301 size_t aboutTextLength;
302};
303
310static void SDL_Libretro_MenuResumeClicked(SDL_LibretroMenu* menu, void* userdata) {
311 (void)userdata;
312 if (SDL_Libretro_IsGameReady(menu->lr)) {
313 SDL_Libretro_SetMenuOpen(menu, false);
314 }
315}
316
321static void SDL_Libretro_MenuBackClicked(nk_console* widget, void* user_data) {
322 (void)widget;
323 SDL_Libretro_MenuResumeClicked((SDL_LibretroMenu*)user_data, NULL);
324}
325
330static void SDL_Libretro_MenuLoadPendingGame(SDL_LibretroMenu* menu) {
331 if (menu->loadGamePath[0] == '\0') {
332 return;
333 }
334
335 // Drop the previous core so the loader picks the right one by extension.
336 SDL_Libretro_UnloadCore(menu->lr);
337
338#if defined(SDL_LIBRETRO_ENABLE_PHYSFS) && !defined(SDL_LIBRETRO_DISABLE_PHYSFS)
339 bool loaded = SDL_Libretro_PhysFS_LoadGame(menu->lr, menu->loadGamePath);
340#else
341 bool loaded = SDL_Libretro_LoadGame(menu->lr, menu->loadGamePath);
342#endif
343 if (loaded) {
344 // The load path already pushed SDL_EVENT_LIBRETRO_GAME_LOADED.
345 SDL_Libretro_SetMenuOpen(menu, false);
346 }
347 else {
348 SDL_Log("Failed to load game: %s", SDL_GetError());
349 nk_console_show_message(menu->console, "Failed to load game");
350 }
351 menu->loadGamePath[0] = '\0';
352}
353
354#ifndef __EMSCRIPTEN__
358static void SDL_Libretro_MenuGameFileChanged(nk_console* widget, void* user_data) {
359 (void)widget;
360 SDL_Libretro_MenuLoadPendingGame((SDL_LibretroMenu*)user_data);
361}
362#endif
363
364#ifdef __EMSCRIPTEN__
372EM_JS(void, SDL_Libretro_MenuOpenWebFilePicker, (SDL_LibretroMenu * menu), {
373 var input = document.createElement('input');
374 input.type = 'file';
375 input.onchange = function(e) {
376 var file = e.target.files[0];
377 if (!file) return;
378 var reader = new FileReader();
379 reader.onload = function() {
380 var path = '/' + file.name;
381 try {
382 Module.FS.writeFile(path, new Uint8Array(reader.result));
383 } catch (err) {
384 console.error('SDL_libretro: failed to write picked file to FS:', err);
385 return;
386 }
387 Module.ccall('SDL_Libretro_MenuLoadGameFromJS', null, ['number', 'string'], [menu, path]);
388 };
389 reader.onerror = function() {
390 console.error('SDL_libretro: failed to read picked file:', reader.error);
391 };
392 reader.readAsArrayBuffer(file);
393 };
394 input.click();
395});
396
402EMSCRIPTEN_KEEPALIVE void SDL_Libretro_MenuLoadGameFromJS(SDL_LibretroMenu* menu, const char* path) {
403 if (menu == NULL || path == NULL) {
404 return;
405 }
406 SDL_strlcpy(menu->loadGamePath, path, sizeof(menu->loadGamePath));
407 SDL_Libretro_MenuLoadPendingGame(menu);
408}
409
413static void SDL_Libretro_MenuWebLoadGameClicked(SDL_LibretroMenu* menu, void* userdata) {
414 (void)userdata;
415 SDL_Libretro_MenuOpenWebFilePicker(menu);
416}
417#endif /* __EMSCRIPTEN__ */
418
422static void SDL_Libretro_MenuSaveStateClicked(SDL_LibretroMenu* menu, void* userdata) {
423 (void)userdata;
424 char savePath[SDL_LIBRETRO_MAX_PATH];
425 if (SDL_Libretro_GetSavePath(menu->lr, ".state", savePath, sizeof(savePath)) > 0 &&
426 SDL_Libretro_SaveState(menu->lr, savePath)) {
427 nk_console_show_message(menu->console, "State saved");
428 }
429 else {
430 nk_console_show_message(menu->console, "Save State failed");
431 }
432}
433
437static void SDL_Libretro_MenuLoadStateClicked(SDL_LibretroMenu* menu, void* userdata) {
438 (void)userdata;
439 char savePath[SDL_LIBRETRO_MAX_PATH];
440 if (SDL_Libretro_GetSavePath(menu->lr, ".state", savePath, sizeof(savePath)) > 0 &&
441 SDL_Libretro_LoadState(menu->lr, savePath)) {
442 SDL_Libretro_SetMenuOpen(menu, false);
443 }
444 else {
445 nk_console_show_message(menu->console, "Load State failed");
446 }
447}
448
462void SDL_Libretro_MenuScreenshotClicked(SDL_LibretroMenu* menu, void* userdata) {
463 if (menu == NULL) {
464 return;
465 }
466 if (SDL_Libretro_SaveScreenshot(menu->lr, (const char*)userdata)) {
467 nk_console_show_message(menu->console, "Screenshot saved");
468 }
469 else {
470 SDL_Log("Failed to save screenshot: %s", SDL_GetError());
471 nk_console_show_message(menu->console, "Screenshot failed");
472 }
473}
474
478static void SDL_Libretro_MenuResetClicked(SDL_LibretroMenu* menu, void* userdata) {
479 (void)userdata;
480 if (SDL_Libretro_Reset(menu->lr)) {
481 SDL_Libretro_SetMenuOpen(menu, false);
482 }
483}
484
489static const char* SDL_Libretro_MenuPixelFormatName(enum retro_pixel_format format) {
490 switch (format) {
491 case RETRO_PIXEL_FORMAT_0RGB1555:
492 return "0RGB1555";
493 case RETRO_PIXEL_FORMAT_XRGB8888:
494 return "XRGB8888";
495 case RETRO_PIXEL_FORMAT_RGB565:
496 return "RGB565";
497 default:
498 return "Unknown";
499 }
500}
501
506static void SDL_Libretro_MenuAddBackButton(nk_console* parent, const char* label) {
507 nk_console_button_set_symbol(
508 nk_console_button_onclick(parent, label, &nk_console_button_back),
509 NK_SYMBOL_TRIANGLE_UP);
510}
511
516static nk_console* SDL_Libretro_MenuAddSubmenu(nk_console* parent, const char* label, enum nk_symbol_type symbol) {
517 nk_console* button = nk_console_button(parent, label);
518 nk_console_button_set_symbol(button, symbol);
519 SDL_Libretro_MenuAddBackButton(button, label);
520 return button;
521}
522
529static nk_console* SDL_Libretro_MenuOnChanged(nk_console* widget, nk_console_event callback, void* user_data) {
530 nk_console_add_event_handler(widget, NK_CONSOLE_EVENT_CHANGED, callback, user_data, NULL);
531 return widget;
532}
533
539static void SDL_Libretro_MenuGuardActiveParent(SDL_LibretroMenu* menu, nk_console* root) {
540 nk_console* active = nk_console_active_parent(menu->console);
541 for (nk_console* it = active; it != NULL; it = it->parent) {
542 if (it == root) {
543 if (active != root) {
544 nk_console_set_active_parent(root);
545 }
546 return;
547 }
548 }
549}
550
558typedef struct SDL_LibretroMenuItem {
559 SDL_LibretroMenu* menu;
561 void* userdata;
562 char* label;
563 nk_bool checked;
564 bool* value;
565} SDL_LibretroMenuItem;
566
570static void SDL_Libretro_MenuItemChanged(nk_console* widget, void* user_data) {
571 (void)widget;
572 SDL_LibretroMenuItem* item = (SDL_LibretroMenuItem*)user_data;
573 if (item->value != NULL) {
574 *item->value = item->checked == nk_true;
575 }
576 if (item->callback != NULL) {
577 item->callback(item->menu, item->userdata);
578 }
579}
580
584static void SDL_Libretro_MenuItemDestroy(nk_console* widget, void* user_data) {
585 (void)widget;
586 SDL_LibretroMenuItem* item = (SDL_LibretroMenuItem*)user_data;
587 SDL_free(item->label);
588 SDL_free(item);
589}
590
594static SDL_LibretroMenuItem* SDL_Libretro_MenuCreateItem(SDL_LibretroMenu* menu, const char* label, SDL_LibretroMenuCallback callback, void* userdata) {
595 SDL_LibretroMenuItem* item = (SDL_LibretroMenuItem*)SDL_calloc(1, sizeof(SDL_LibretroMenuItem));
596 if (item == NULL) {
597 return NULL;
598 }
599 item->menu = menu;
600 item->callback = callback;
601 item->userdata = userdata;
602 item->label = SDL_strdup(label);
603 if (item->label == NULL) {
604 SDL_free(item);
605 return NULL;
606 }
607 return item;
608}
609
618static nk_console* SDL_Libretro_MenuAddButton(SDL_LibretroMenu* menu, nk_console* parent, const char* label, enum nk_symbol_type symbol, SDL_LibretroMenuCallback callback, void* userdata) {
619 SDL_LibretroMenuItem* item = SDL_Libretro_MenuCreateItem(menu, label, callback, userdata);
620 if (item == NULL) {
621 return NULL;
622 }
623 nk_console* button = nk_console_button_onclick_handler(parent, item->label, &SDL_Libretro_MenuItemChanged, item, &SDL_Libretro_MenuItemDestroy);
624 if (button == NULL) {
625 SDL_Libretro_MenuItemDestroy(NULL, item);
626 return NULL;
627 }
628 nk_console_button_set_symbol(button, symbol);
629 return button;
630}
631
641static nk_console* SDL_Libretro_MenuAddCheckbox(SDL_LibretroMenu* menu, nk_console* parent, const char* label, nk_bool* checked, bool* value, SDL_LibretroMenuCallback callback, void* userdata) {
642 SDL_LibretroMenuItem* item = SDL_Libretro_MenuCreateItem(menu, label, callback, userdata);
643 if (item == NULL) {
644 return NULL;
645 }
646 nk_bool* storage = checked != NULL ? checked : &item->checked;
647 item->value = value;
648 if (value != NULL) {
649 *storage = (nk_bool)*value;
650 }
651 nk_console* checkbox = nk_console_checkbox(parent, item->label, storage);
652 if (checkbox == NULL) {
653 SDL_Libretro_MenuItemDestroy(NULL, item);
654 return NULL;
655 }
656 nk_console_add_event_handler(checkbox, NK_CONSOLE_EVENT_CHANGED, &SDL_Libretro_MenuItemChanged, item, &SDL_Libretro_MenuItemDestroy);
657 return checkbox;
658}
659
665static void SDL_Libretro_MenuAboutLine(SDL_LibretroMenu* menu, const char* format, ...) {
666 char* line = menu->aboutText + menu->aboutTextLength;
667 size_t available = sizeof(menu->aboutText) - menu->aboutTextLength;
668 if (available < 2) {
669 return;
670 }
671 va_list args;
672 va_start(args, format);
673 int written = SDL_vsnprintf(line, available, format, args);
674 va_end(args);
675 if (written < 0) {
676 return;
677 }
678 if ((size_t)written >= available) {
679 written = (int)available - 1;
680 }
681 menu->aboutTextLength += (size_t)written + 1;
682 nk_console_label(menu->aboutButton, line);
683}
684
690static void SDL_Libretro_MenuBuildAbout(SDL_LibretroMenu* menu) {
691 SDL_Libretro* lr = menu->lr;
692
693 nk_console_free_children(menu->aboutButton);
694 menu->aboutTextLength = 0;
695 SDL_Libretro_MenuAddBackButton(menu->aboutButton, "About");
696
697 // Frontend
698 SDL_Libretro_MenuAboutLine(menu, "SDL_libretro %d.%d.%d", SDL_LIBRETRO_MAJOR_VERSION, SDL_LIBRETRO_MINOR_VERSION, SDL_LIBRETRO_MICRO_VERSION);
699 int sdlVersion = SDL_GetVersion();
700 SDL_Libretro_MenuAboutLine(menu, "SDL %d.%d.%d %s", SDL_VERSIONNUM_MAJOR(sdlVersion), SDL_VERSIONNUM_MINOR(sdlVersion), SDL_VERSIONNUM_MICRO(sdlVersion), SDL_GetRevision());
701 const char* rendererName = SDL_GetRendererName(lr->renderer);
702 SDL_Libretro_MenuAboutLine(menu, "Renderer: %s", rendererName != NULL ? rendererName : "Unknown");
703
704 // Core
705 if (SDL_Libretro_IsCoreReady(lr)) {
706 SDL_Libretro_MenuAboutLine(menu, "Core: %s", SDL_Libretro_GetCoreName(lr));
707 const char* coreVersion = SDL_Libretro_GetCoreVersion(lr);
708 if (coreVersion[0] != '\0') {
709 SDL_Libretro_MenuAboutLine(menu, "Core Version: %s", coreVersion);
710 }
711 const char* extensions = SDL_Libretro_GetValidExtensions(lr);
712 SDL_Libretro_MenuAboutLine(menu, "Extensions: %s", extensions[0] != '\0' ? extensions : "(any)");
713 SDL_Libretro_MenuAboutLine(menu, "Pixel Format: %s", SDL_Libretro_MenuPixelFormatName(lr->core.pixelFormat));
714 if (lr->core.sampleRate > 0.0) {
715 SDL_Libretro_MenuAboutLine(menu, "Sample Rate: %g Hz", lr->core.sampleRate);
716 }
717 }
718
719 // Content
720 if (SDL_Libretro_IsGameReady(lr)) {
721 char contentName[128] = "";
722 SDL_Libretro_GetFileName(contentName, sizeof(contentName), lr->core.contentPath, true);
723 SDL_Libretro_MenuAboutLine(menu, "Content: %s", contentName[0] != '\0' ? contentName : "(none)");
724 int width = 0;
725 int height = 0;
726 SDL_Libretro_GetSize(lr, &width, &height);
727 SDL_Libretro_MenuAboutLine(menu, "Size: %dx%d @ %.2f FPS", width, height, SDL_Libretro_GetFPS(lr));
728 int rotation = SDL_Libretro_GetRotation(lr);
729 if (rotation != 0) {
730 SDL_Libretro_MenuAboutLine(menu, "Aspect Ratio: %.3f (rotated %d)", SDL_Libretro_GetAspectRatio(lr), rotation * 90);
731 }
732 else {
733 SDL_Libretro_MenuAboutLine(menu, "Aspect Ratio: %.3f", SDL_Libretro_GetAspectRatio(lr));
734 }
735 }
736
737 // The cores in the core directory that can be loaded: a .info file
738 // without its core library beside it doesn't count.
739 unsigned availableCores = 0;
740 for (unsigned i = 0; i < lr->coreLibraryCount; i++) {
741 availableCores += SDL_GetPathInfo(lr->coreLibrary[i].path, NULL) ? 1 : 0;
742 }
743 SDL_Libretro_MenuAboutLine(menu, "Available Cores: %u", availableCores);
744 for (unsigned i = 0; i < lr->coreLibraryCount; i++) {
745 if (SDL_GetPathInfo(lr->coreLibrary[i].path, NULL)) {
746 SDL_Libretro_MenuAboutLine(menu, " %s", lr->coreLibrary[i].corename);
747 }
748 }
749
750 nk_console_label(menu->aboutButton, "https://github.com/RobLoach/SDL_libretro");
751}
752
758static void SDL_Libretro_MenuAboutOpened(SDL_LibretroMenu* menu, void* userdata) {
759 (void)userdata;
760 SDL_Libretro_MenuBuildAbout(menu);
761 nk_console_set_active_parent(menu->aboutButton);
762}
763
767static void SDL_Libretro_MenuQuitClicked(SDL_LibretroMenu* menu, void* userdata) {
768 (void)menu;
769 (void)userdata;
770 SDL_Event event;
771 SDL_zero(event);
772 event.type = SDL_EVENT_QUIT;
773 event.quit.timestamp = SDL_GetTicksNS();
774 SDL_PushEvent(&event);
775}
776
780static void SDL_Libretro_MenuVolumeChanged(nk_console* widget, void* user_data) {
781 (void)widget;
782 SDL_LibretroMenu* menu = (SDL_LibretroMenu*)user_data;
783 SDL_Libretro_SetVolume(menu->lr, (float)menu->volumePercent / 100.0f);
784}
785
789static void SDL_Libretro_MenuFitModeChanged(nk_console* widget, void* user_data) {
790 (void)widget;
791 SDL_LibretroMenu* menu = (SDL_LibretroMenu*)user_data;
792 SDL_Libretro_SetFitMode(menu->lr, (SDL_LibretroFitMode)menu->fitModeIndex);
793}
794
798static void SDL_Libretro_MenuFilterChanged(nk_console* widget, void* user_data) {
799 (void)widget;
800 SDL_LibretroMenu* menu = (SDL_LibretroMenu*)user_data;
801 SDL_Libretro_SetScaleMode(menu->lr, menu->filterIndex == 1 ? SDL_SCALEMODE_LINEAR : SDL_SCALEMODE_NEAREST);
802}
803
807static void SDL_Libretro_MenuFullscreenChanged(SDL_LibretroMenu* menu, void* userdata) {
808 (void)userdata;
809 SDL_SetWindowFullscreen(menu->lr->window, menu->fullscreenChecked == nk_true);
810 menu->settingsDirty = true;
811}
812
816static void SDL_Libretro_MenuVSyncChanged(SDL_LibretroMenu* menu, void* userdata) {
817 (void)userdata;
818 SDL_SetRenderVSync(menu->lr->renderer, menu->vsyncChecked == nk_true ? 1 : 0);
819 menu->settingsDirty = true;
820}
821
825static void SDL_Libretro_MenuRewindChanged(SDL_LibretroMenu* menu, void* userdata) {
826 (void)userdata;
827 // Keep the capacity and interval the app configured.
828 SDL_Libretro_SetRewindEnabled(menu->lr, menu->rewindChecked == nk_true, menu->lr->rewindCapacity, menu->lr->rewindCaptureInterval);
829}
830
834static void SDL_Libretro_MenuMousePortChanged(nk_console* widget, void* user_data) {
835 (void)widget;
836 SDL_LibretroMenu* menu = (SDL_LibretroMenu*)user_data;
837 SDL_Libretro_SetMousePort(menu->lr, (unsigned)menu->mousePortIndex);
838}
839
843static void SDL_Libretro_MenuLanguageChanged(nk_console* widget, void* user_data) {
844 (void)widget;
845 SDL_LibretroMenu* menu = (SDL_LibretroMenu*)user_data;
846 SDL_Libretro_SetLanguage(menu->lr, menu->languageIndex - 1);
847}
848
852static void SDL_Libretro_MenuRewindBufferChanged(nk_console* widget, void* user_data) {
853 (void)widget;
854 SDL_LibretroMenu* menu = (SDL_LibretroMenu*)user_data;
855 SDL_Libretro_SetRewindMemoryLimit(menu->lr, (size_t)menu->rewindBufferMB << 20);
856}
857
862static void SDL_Libretro_MenuMuteChanged(SDL_LibretroMenu* menu, void* userdata) {
863 (void)userdata;
864 SDL_Libretro* lr = menu->lr;
865 if (menu->muteChecked) {
866 menu->preMuteVolume = SDL_Libretro_GetVolume(lr);
867 SDL_Libretro_SetVolume(lr, 0.0f);
868 }
869 else {
870 SDL_Libretro_SetVolume(lr, menu->preMuteVolume > 0.0f ? menu->preMuteVolume : 1.0f);
871 menu->volumePercent = (int)(SDL_Libretro_GetVolume(lr) * 100.0f + 0.5f);
872 }
873 menu->settingsDirty = true;
874}
875
884static const struct {
885 SDL_Scancode scancode;
886 nk_rune rune;
887} SDL_Libretro_MenuKeyRunes[] = {
888 {SDL_SCANCODE_RETURN, NK_CONSOLE_KEY_ENTER},
889 {SDL_SCANCODE_KP_ENTER, NK_CONSOLE_KEY_ENTER},
890 {SDL_SCANCODE_TAB, NK_CONSOLE_KEY_TAB},
891 {SDL_SCANCODE_BACKSPACE, NK_CONSOLE_KEY_BACKSPACE},
892 {SDL_SCANCODE_ESCAPE, NK_CONSOLE_KEY_ESCAPE},
893 {SDL_SCANCODE_DELETE, NK_CONSOLE_KEY_DELETE},
894 {SDL_SCANCODE_UP, NK_CONSOLE_KEY_UP},
895 {SDL_SCANCODE_DOWN, NK_CONSOLE_KEY_DOWN},
896 {SDL_SCANCODE_LEFT, NK_CONSOLE_KEY_LEFT},
897 {SDL_SCANCODE_RIGHT, NK_CONSOLE_KEY_RIGHT},
898 {SDL_SCANCODE_LSHIFT, NK_CONSOLE_KEY_SHIFT},
899 {SDL_SCANCODE_RSHIFT, NK_CONSOLE_KEY_SHIFT},
900 {SDL_SCANCODE_LCTRL, NK_CONSOLE_KEY_CTRL},
901 {SDL_SCANCODE_RCTRL, NK_CONSOLE_KEY_CTRL},
902 {SDL_SCANCODE_LALT, NK_CONSOLE_KEY_ALT},
903 {SDL_SCANCODE_RALT, NK_CONSOLE_KEY_ALT},
904 {SDL_SCANCODE_F1, NK_CONSOLE_KEY_F1},
905 {SDL_SCANCODE_F2, NK_CONSOLE_KEY_F2},
906 {SDL_SCANCODE_F3, NK_CONSOLE_KEY_F3},
907 {SDL_SCANCODE_F4, NK_CONSOLE_KEY_F4},
908 {SDL_SCANCODE_F5, NK_CONSOLE_KEY_F5},
909 {SDL_SCANCODE_F6, NK_CONSOLE_KEY_F6},
910 {SDL_SCANCODE_F7, NK_CONSOLE_KEY_F7},
911 {SDL_SCANCODE_F8, NK_CONSOLE_KEY_F8},
912 {SDL_SCANCODE_F9, NK_CONSOLE_KEY_F9},
913 {SDL_SCANCODE_F10, NK_CONSOLE_KEY_F10},
914 {SDL_SCANCODE_F11, NK_CONSOLE_KEY_F11},
915 {SDL_SCANCODE_F12, NK_CONSOLE_KEY_F12},
916};
917
924static nk_rune SDL_Libretro_MenuRuneFromScancode(SDL_Scancode scancode) {
925 for (size_t i = 0; i < SDL_arraysize(SDL_Libretro_MenuKeyRunes); i++) {
926 if (SDL_Libretro_MenuKeyRunes[i].scancode == scancode) {
927 return SDL_Libretro_MenuKeyRunes[i].rune;
928 }
929 }
930 SDL_Keycode key = SDL_GetKeyFromScancode(scancode, SDL_KMOD_NONE, false);
931 if (key >= 32 && key < 127) {
932 return (nk_rune)key;
933 }
934 return NK_CONSOLE_KEY_NONE;
935}
936
942static SDL_Scancode SDL_Libretro_MenuScancodeFromRune(nk_rune rune) {
943 for (size_t i = 0; i < SDL_arraysize(SDL_Libretro_MenuKeyRunes); i++) {
944 if (SDL_Libretro_MenuKeyRunes[i].rune == rune) {
945 return SDL_Libretro_MenuKeyRunes[i].scancode;
946 }
947 }
948 if (rune >= 'A' && rune <= 'Z') {
949 rune += 'a' - 'A';
950 }
951 if (rune >= 32 && rune < 127) {
952 return SDL_GetScancodeFromKey((SDL_Keycode)rune, NULL);
953 }
954 return SDL_SCANCODE_UNKNOWN;
955}
956
961static void SDL_Libretro_MenuSyncKeyBinds(SDL_LibretroMenu* menu) {
962 for (int i = 0; i < SDL_LIBRETRO_MAX_JOYPAD_BUTTONS; i++) {
963 menu->keyBinds[i].rune = SDL_Libretro_MenuRuneFromScancode(menu->lr->keyboardPlayer1[i]);
964 }
965}
966
973static void SDL_Libretro_MenuKeyBindChanged(nk_console* widget, void* user_data) {
974 (void)widget;
975 SDL_LibretroMenuKeyBind* bind = (SDL_LibretroMenuKeyBind*)user_data;
976 SDL_LibretroMenu* menu = bind->menu;
977 SDL_Scancode scancode = SDL_Libretro_MenuScancodeFromRune(bind->rune);
978 if (scancode == SDL_SCANCODE_UNKNOWN) {
979 bind->rune = SDL_Libretro_MenuRuneFromScancode(menu->lr->keyboardPlayer1[bind->button]);
980 return;
981 }
982 SDL_Libretro_SetKeyboardMapping(menu->lr, bind->button, scancode);
983}
984
991static enum nk_gamepad_button SDL_Libretro_MenuNkButtonFromSDL(SDL_GamepadButton button) {
992 for (int i = NK_GAMEPAD_BUTTON_FIRST; i <= NK_GAMEPAD_BUTTON_R3; i++) {
993 if (nk_gamepad_sdl3_map_button(i) == button) {
994 return (enum nk_gamepad_button)i;
995 }
996 }
997 return NK_GAMEPAD_BUTTON_INVALID;
998}
999
1005static void SDL_Libretro_MenuSyncPadBinds(SDL_LibretroMenu* menu) {
1006 for (unsigned port = 0; port < SDL_LIBRETRO_MAX_USERS; port++) {
1007 SDL_LibretroMenuPadPort* padPort = &menu->padPorts[port];
1008 padPort->analogIndex = (int)menu->lr->analogToDigital[port];
1009 for (int i = 0; i < SDL_LIBRETRO_MAX_JOYPAD_BUTTONS; i++) {
1010 padPort->binds[i].captured = SDL_Libretro_MenuNkButtonFromSDL(menu->lr->gamepadButtons[port][i]);
1011 }
1012 }
1013}
1014
1021static void SDL_Libretro_MenuPadBindChanged(nk_console* widget, void* user_data) {
1022 (void)widget;
1023 SDL_LibretroMenuPadBind* bind = (SDL_LibretroMenuPadBind*)user_data;
1024 SDL_LibretroMenu* menu = bind->menu;
1025 SDL_GamepadButton button = nk_gamepad_sdl3_map_button((int)bind->captured);
1026 if (button == SDL_GAMEPAD_BUTTON_INVALID) {
1027 bind->captured = SDL_Libretro_MenuNkButtonFromSDL(menu->lr->gamepadButtons[bind->port][bind->button]);
1028 return;
1029 }
1030 SDL_Libretro_SetGamepadMapping(menu->lr, bind->port, bind->button, button);
1031}
1032
1037static void SDL_Libretro_MenuAnalogToDigitalChanged(nk_console* widget, void* user_data) {
1038 (void)widget;
1039 SDL_LibretroMenuPadPort* padPort = (SDL_LibretroMenuPadPort*)user_data;
1040 SDL_Libretro_SetAnalogToDigital(padPort->menu->lr, padPort->port, (SDL_LibretroAnalogToDigital)padPort->analogIndex);
1041}
1042
1050static void SDL_Libretro_MenuBuildGamepads(SDL_LibretroMenu* menu) {
1051 if (menu->gamepadButton == NULL) {
1052 return;
1053 }
1054 SDL_Libretro* lr = menu->lr;
1055
1056 SDL_Libretro_MenuGuardActiveParent(menu, menu->gamepadButton);
1057 nk_console_free_children(menu->gamepadButton);
1058 SDL_Libretro_MenuAddBackButton(menu->gamepadButton, "Gamepad");
1059
1060 SDL_Libretro_MenuSyncPadBinds(menu);
1061 for (unsigned port = 0; port < SDL_LIBRETRO_MAX_USERS; port++) {
1062 SDL_LibretroMenuPadPort* padPort = &menu->padPorts[port];
1063 padPort->menu = menu;
1064 padPort->port = port;
1065
1066 const char* name = lr->gamepads[port] != NULL ? SDL_GetGamepadName(lr->gamepads[port]) : NULL;
1067 SDL_snprintf(padPort->label, sizeof(padPort->label), "%u. %s", port + 1, name != NULL ? name : "No Gamepad");
1068 nk_console* player = SDL_Libretro_MenuAddSubmenu(menu->gamepadButton, padPort->label, NK_SYMBOL_TRIANGLE_RIGHT);
1069 nk_console_set_tooltip(player, "Gamepad buttons for this player's retro controller; the triggers stay analog");
1070
1071 nk_console_set_tooltip(
1072 SDL_Libretro_MenuOnChanged(nk_console_combobox(player, "Analog to Digital", "None|Left|Right", '|', &padPort->analogIndex), &SDL_Libretro_MenuAnalogToDigitalChanged, padPort),
1073 "The chosen analog stick also presses the D-Pad directions");
1074
1075 for (int i = 0; i < SDL_LIBRETRO_MAX_JOYPAD_BUTTONS; i++) {
1076 padPort->binds[i].menu = menu;
1077 padPort->binds[i].port = port;
1078 padPort->binds[i].button = i;
1079 SDL_Libretro_MenuOnChanged(
1080 nk_console_input_gamepad(player, SDL_Libretro_JoypadButtonNames[i], -1, NULL, &padPort->binds[i].captured),
1081 &SDL_Libretro_MenuPadBindChanged,
1082 &padPort->binds[i]);
1083 }
1084 }
1085}
1086
1095static void SDL_Libretro_MenuLoadState(SDL_LibretroMenu* menu) {
1096 SDL_Libretro* lr = menu->lr;
1098 // Default the UI scale to 2x; a saved choice (including an explicit Auto)
1099 // overrides it below.
1100 menu->uiScaleIndex = 2;
1101 if (lr->ini != NULL) {
1102 Sint64 savedStyle = INI_GetInt(lr->ini, NULL, "menutheme", (Sint64)style);
1103 if (savedStyle >= 0 && savedStyle < (Sint64)SDL_LIBRETRO_MENU_STYLE_COUNT) {
1104 style = (SDL_LibretroMenuStyle)savedStyle;
1105 }
1106 if (INI_HasValue(lr->ini, NULL, "menuuiscale")) {
1107 Sint64 savedScale = INI_GetInt(lr->ini, NULL, "menuuiscale", 2);
1108 if (savedScale >= 0 && savedScale <= 4) {
1109 menu->uiScaleIndex = (int)savedScale;
1110 }
1111 }
1112 if (INI_HasValue(lr->ini, NULL, "menufullscreen")) {
1113 SDL_SetWindowFullscreen(lr->window, INI_GetBoolean(lr->ini, NULL, "menufullscreen", false));
1114 }
1115 if (INI_HasValue(lr->ini, NULL, "menuvsync")) {
1116 SDL_SetRenderVSync(lr->renderer, INI_GetBoolean(lr->ini, NULL, "menuvsync", false) ? 1 : 0);
1117 }
1118 if (INI_GetBoolean(lr->ini, NULL, "menumuted", false)) {
1119 menu->muteChecked = nk_true;
1120 menu->preMuteVolume = INI_GetFloat(lr->ini, NULL, "menumutevolume", 1.0f);
1121 SDL_Libretro_SetVolume(lr, 0.0f);
1122 }
1123 }
1124 SDL_Libretro_SetMenuStyle(menu, style);
1125}
1126
1135static void SDL_Libretro_MenuSaveState(SDL_LibretroMenu* menu) {
1136 SDL_Libretro* lr = menu->lr;
1137 menu->settingsDirty = false;
1138 if (lr->ini == NULL) {
1139 return;
1140 }
1141 INI_SetInt(lr->ini, NULL, "menutheme", (Sint64)menu->style);
1142 INI_SetInt(lr->ini, NULL, "menuuiscale", (Sint64)menu->uiScaleIndex);
1143 INI_SetBoolean(lr->ini, NULL, "menufullscreen", menu->fullscreenChecked == nk_true);
1144 INI_SetBoolean(lr->ini, NULL, "menuvsync", menu->vsyncChecked == nk_true);
1145 INI_SetBoolean(lr->ini, NULL, "menumuted", menu->muteChecked == nk_true);
1146 INI_SetFloat(lr->ini, NULL, "menumutevolume", menu->preMuteVolume);
1147}
1148
1153static bool SDL_Libretro_MenuValueTruthy(const char* value) {
1154 return value != NULL && (SDL_strcasecmp(value, "enabled") == 0 ||
1155 SDL_strcasecmp(value, "true") == 0 ||
1156 SDL_strcasecmp(value, "on") == 0 ||
1157 SDL_strcasecmp(value, "yes") == 0 ||
1158 SDL_strcmp(value, "1") == 0);
1159}
1160
1165static bool SDL_Libretro_MenuValueFalsy(const char* value) {
1166 return value != NULL && (SDL_strcasecmp(value, "disabled") == 0 ||
1167 SDL_strcasecmp(value, "false") == 0 ||
1168 SDL_strcasecmp(value, "off") == 0 ||
1169 SDL_strcasecmp(value, "no") == 0 ||
1170 SDL_strcmp(value, "0") == 0);
1171}
1172
1177static bool SDL_Libretro_MenuOptionIsBoolean(const SDL_LibretroOption* option) {
1178 if (option->valuesCount != 2) {
1179 return false;
1180 }
1181 const char* first = option->values[0].value;
1182 const char* second = option->values[1].value;
1183 return (SDL_Libretro_MenuValueTruthy(first) && SDL_Libretro_MenuValueFalsy(second)) ||
1184 (SDL_Libretro_MenuValueFalsy(first) && SDL_Libretro_MenuValueTruthy(second));
1185}
1186
1192static void SDL_Libretro_MenuOptionWritten(SDL_LibretroMenu* menu) {
1196 menu->optionsStale = true;
1197 }
1198}
1199
1203static void SDL_Libretro_MenuOptionChanged(nk_console* widget, void* user_data) {
1204 (void)widget;
1205 SDL_LibretroMenuOptionState* state = (SDL_LibretroMenuOptionState*)user_data;
1206 SDL_LibretroMenu* menu = state->menu;
1207 const SDL_LibretroOption* option = SDL_Libretro_GetOptionByIndex(menu->lr, state->index);
1208 if (option == NULL || state->selected < 0 || (unsigned)state->selected >= option->valuesCount) {
1209 return;
1210 }
1211 SDL_Libretro_SetOptionValue(menu->lr, option->key, option->values[state->selected].value);
1212 SDL_Libretro_MenuOptionWritten(menu);
1213}
1214
1218static void SDL_Libretro_MenuOptionCheckboxChanged(SDL_LibretroMenu* menu, void* userdata) {
1219 SDL_LibretroMenuOptionState* state = (SDL_LibretroMenuOptionState*)userdata;
1220 const SDL_LibretroOption* option = SDL_Libretro_GetOptionByIndex(menu->lr, state->index);
1221 if (option == NULL || option->valuesCount != 2) {
1222 return;
1223 }
1224 // Write back the token the core declared rather than assuming a fixed pair.
1225 bool firstTruthy = SDL_Libretro_MenuValueTruthy(option->values[0].value);
1226 const char* truthy = firstTruthy ? option->values[0].value : option->values[1].value;
1227 const char* falsy = firstTruthy ? option->values[1].value : option->values[0].value;
1228 SDL_Libretro_SetOptionValue(menu->lr, option->key, state->checked ? truthy : falsy);
1229 SDL_Libretro_MenuOptionWritten(menu);
1230}
1231
1235static void SDL_Libretro_MenuResetOptionsClicked(SDL_LibretroMenu* menu, void* userdata) {
1236 (void)userdata;
1238 SDL_Libretro_MenuOptionWritten(menu);
1239 menu->optionsStale = true;
1240 nk_console_show_message(menu->console, "Core options reset to defaults");
1241}
1242
1246static void SDL_Libretro_MenuFreeOptionStates(SDL_LibretroMenu* menu) {
1247 if (menu->optionStates != NULL) {
1248 for (unsigned i = 0; i < menu->optionStateCount; i++) {
1249 SDL_free(menu->optionStates[i].displayList);
1250 }
1251 SDL_free(menu->optionStates);
1252 menu->optionStates = NULL;
1253 }
1254 menu->optionStateCount = 0;
1255}
1256
1265static char* SDL_Libretro_MenuJoinPipeList(unsigned count, const char* (*item)(void* userdata, unsigned index), void* userdata) {
1266 size_t length = 0;
1267 for (unsigned i = 0; i < count; i++) {
1268 length += SDL_strlen(item(userdata, i)) + 1;
1269 }
1270 char* list = (char*)SDL_malloc(length + 1);
1271 if (list == NULL) {
1272 return NULL;
1273 }
1274 list[0] = '\0';
1275 size_t offset = 0;
1276 for (unsigned i = 0; i < count; i++) {
1277 if (i > 0) {
1278 list[offset++] = '|';
1279 }
1280 offset += SDL_strlcpy(list + offset, item(userdata, i), length + 1 - offset);
1281 }
1282 return list;
1283}
1284
1289static bool SDL_Libretro_MenuOptionShown(const SDL_LibretroOption* option) {
1290 return option != NULL && option->visible && option->valuesCount > 0;
1291}
1292
1299static const char* SDL_Libretro_MenuOptionValueText(void* userdata, unsigned index) {
1300 const SDL_LibretroOption* option = (const SDL_LibretroOption*)userdata;
1301 const char* label = option->values[index].label;
1302 return (label != NULL && label[0] != '\0') ? label : option->values[index].value;
1303}
1304
1309static void SDL_Libretro_MenuBuildOptionWidget(SDL_LibretroMenu* menu, nk_console* parent, unsigned index) {
1310 const SDL_LibretroOption* option = SDL_Libretro_GetOptionByIndex(menu->lr, index);
1311 SDL_LibretroMenuOptionState* state = &menu->optionStates[index];
1312 state->menu = menu;
1313 state->index = index;
1314
1315 const char* label = (option->desc != NULL && option->desc[0] != '\0') ? option->desc : option->key;
1316 nk_console* widget;
1317
1318 if (SDL_Libretro_MenuOptionIsBoolean(option)) {
1319 state->checked = (nk_bool)SDL_Libretro_MenuValueTruthy(option->value);
1320 widget = SDL_Libretro_MenuAddCheckbox(menu, parent, label, &state->checked, NULL, &SDL_Libretro_MenuOptionCheckboxChanged, state);
1321 }
1322 else {
1323 // The combobox reads the pipe-separated list lazily, so keep it alive in the state.
1324 state->displayList = SDL_Libretro_MenuJoinPipeList(option->valuesCount, &SDL_Libretro_MenuOptionValueText, (void*)option);
1325 if (state->displayList == NULL) {
1326 return;
1327 }
1328 state->selected = 0;
1329 for (unsigned v = 0; v < option->valuesCount; v++) {
1330 if (option->value != NULL && SDL_strcmp(option->values[v].value, option->value) == 0) {
1331 state->selected = (int)v;
1332 }
1333 }
1334
1335 widget = SDL_Libretro_MenuOnChanged(nk_console_combobox(parent, label, state->displayList, '|', &state->selected), &SDL_Libretro_MenuOptionChanged, state);
1336 }
1337
1338 if (option->info != NULL && option->info[0] != '\0') {
1339 nk_console_set_tooltip(widget, option->info);
1340 }
1341}
1342
1348static int SDL_Libretro_MenuOptionCategory(SDL_Libretro* lr, const SDL_LibretroOption* option) {
1349 if (option->category == NULL || option->category[0] == '\0') {
1350 return -1;
1351 }
1352 unsigned count = SDL_Libretro_GetCategoryCount(lr);
1353 for (unsigned c = 0; c < count; c++) {
1355 if (category != NULL && category->key != NULL && SDL_strcmp(option->category, category->key) == 0) {
1356 return (int)c;
1357 }
1358 }
1359 return -1;
1360}
1361
1366static void SDL_Libretro_MenuBuildOptions(SDL_LibretroMenu* menu) {
1367 if (menu->optionsButton == NULL) {
1368 return;
1369 }
1370 SDL_Libretro* lr = menu->lr;
1371
1372 // Drop the previous widgets before the option states they point into.
1373 SDL_Libretro_MenuGuardActiveParent(menu, menu->optionsButton);
1374 nk_console_free_children(menu->optionsButton);
1375 SDL_Libretro_MenuFreeOptionStates(menu);
1376
1377 unsigned count = SDL_Libretro_GetOptionCount(lr);
1378 unsigned shown = 0;
1379 for (unsigned i = 0; i < count; i++) {
1380 if (SDL_Libretro_MenuOptionShown(SDL_Libretro_GetOptionByIndex(lr, i))) {
1381 shown++;
1382 }
1383 }
1384 if (shown == 0) {
1385 // The page is going away; don't strand the user on it with nothing
1386 // left to navigate, not even a way back.
1387 if (nk_console_active_parent(menu->console) == menu->optionsButton) {
1388 nk_console_set_active_parent(menu->optionsButton->parent);
1389 }
1390 menu->optionsButton->visible = nk_false;
1391 return;
1392 }
1393 menu->optionsButton->visible = nk_true;
1394
1395 menu->optionStates = (SDL_LibretroMenuOptionState*)SDL_calloc(count, sizeof(SDL_LibretroMenuOptionState));
1396 if (menu->optionStates == NULL) {
1397 if (nk_console_active_parent(menu->console) == menu->optionsButton) {
1398 nk_console_set_active_parent(menu->optionsButton->parent);
1399 }
1400 menu->optionsButton->visible = nk_false;
1401 return;
1402 }
1403 menu->optionStateCount = count;
1404
1405 SDL_Libretro_MenuAddBackButton(menu->optionsButton, "Core Options");
1406
1407 // Uncategorized options first, directly under Core Options.
1408 for (unsigned i = 0; i < count; i++) {
1410 if (!SDL_Libretro_MenuOptionShown(option) || SDL_Libretro_MenuOptionCategory(lr, option) >= 0) {
1411 continue;
1412 }
1413 SDL_Libretro_MenuBuildOptionWidget(menu, menu->optionsButton, i);
1414 }
1415
1416 // Then a submenu per category holding its options.
1417 unsigned categoryCount = SDL_Libretro_GetCategoryCount(lr);
1418 for (unsigned c = 0; c < categoryCount; c++) {
1420 bool hasVisible = false;
1421 for (unsigned i = 0; i < count; i++) {
1423 if (SDL_Libretro_MenuOptionShown(option) && SDL_Libretro_MenuOptionCategory(lr, option) == (int)c) {
1424 hasVisible = true;
1425 break;
1426 }
1427 }
1428 if (!hasVisible) {
1429 continue;
1430 }
1431
1432 const char* categoryLabel = (category->desc != NULL && category->desc[0] != '\0') ? category->desc : category->key;
1433 nk_console* categoryButton = SDL_Libretro_MenuAddSubmenu(menu->optionsButton, categoryLabel, NK_SYMBOL_TRIANGLE_RIGHT);
1434 if (category->info != NULL && category->info[0] != '\0') {
1435 nk_console_set_tooltip(categoryButton, category->info);
1436 }
1437
1438 for (unsigned i = 0; i < count; i++) {
1440 if (SDL_Libretro_MenuOptionShown(option) && SDL_Libretro_MenuOptionCategory(lr, option) == (int)c) {
1441 SDL_Libretro_MenuBuildOptionWidget(menu, categoryButton, i);
1442 }
1443 }
1444 }
1445
1446 SDL_Libretro_MenuAddButton(menu, menu->optionsButton, "Reset to Defaults", NK_SYMBOL_TRIANGLE_LEFT, &SDL_Libretro_MenuResetOptionsClicked, NULL);
1447}
1448
1449#ifndef __EMSCRIPTEN__
1458static void SDL_Libretro_MenuAppendExtensions(char* filter, size_t filterSize, const char* extensions) {
1459 if (extensions == NULL) {
1460 return;
1461 }
1462 for (const char* token = extensions; *token != '\0';) {
1463 const char* end = token;
1464 while (*end != '\0' && *end != '|') {
1465 end++;
1466 }
1467 size_t tokenLen = (size_t)(end - token);
1468 char lower[34];
1469 char upper[34];
1470 if (tokenLen > 0 && tokenLen + 2 <= sizeof(lower)) {
1471 lower[0] = '.';
1472 upper[0] = '.';
1473 for (size_t i = 0; i < tokenLen; i++) {
1474 lower[i + 1] = (char)SDL_tolower((int)(unsigned char)token[i]);
1475 upper[i + 1] = (char)SDL_toupper((int)(unsigned char)token[i]);
1476 }
1477 lower[tokenLen + 1] = '\0';
1478 upper[tokenLen + 1] = '\0';
1479
1480 // Skip if the extension is already in the filter.
1481 bool found = false;
1482 for (const char* existing = filter; *existing != '\0' && !found;) {
1483 const char* existingEnd = existing;
1484 while (*existingEnd != '\0' && *existingEnd != ';') {
1485 existingEnd++;
1486 }
1487 size_t existingLen = (size_t)(existingEnd - existing);
1488 if (existingLen == tokenLen + 1 && SDL_strncasecmp(existing, lower, existingLen) == 0) {
1489 found = true;
1490 }
1491 existing = (*existingEnd == ';') ? existingEnd + 1 : existingEnd;
1492 }
1493
1494 if (!found) {
1495 size_t length = SDL_strlen(filter);
1496 // Reserve room for ";lower;UPPER" and the terminator.
1497 if (length + (tokenLen + 2) * 2 + 2 < filterSize) {
1498 if (length > 0) {
1499 filter[length++] = ';';
1500 }
1501 length += SDL_strlcpy(filter + length, lower, filterSize - length);
1502 filter[length++] = ';';
1503 SDL_strlcpy(filter + length, upper, filterSize - length);
1504 }
1505 }
1506 }
1507 token = (*end == '|') ? end + 1 : end;
1508 }
1509}
1510#endif /* !__EMSCRIPTEN__ */
1511
1517static void SDL_Libretro_MenuUpdateLoadGameFilter(SDL_LibretroMenu* menu) {
1518#ifndef __EMSCRIPTEN__
1519 if (menu->loadGameButton == NULL) {
1520 return;
1521 }
1522 SDL_Libretro* lr = menu->lr;
1523
1524 char filter[1024] = "";
1525 for (unsigned i = 0; i < lr->coreLibraryCount; i++) {
1526 SDL_Libretro_MenuAppendExtensions(filter, sizeof(filter), lr->coreLibrary[i].supported_extensions);
1527 }
1528 if (SDL_Libretro_IsCoreReady(lr)) {
1529 SDL_Libretro_MenuAppendExtensions(filter, sizeof(filter), lr->core.validExtensions);
1530 }
1531#if defined(SDL_LIBRETRO_ENABLE_PHYSFS) && !defined(SDL_LIBRETRO_DISABLE_PHYSFS)
1532 SDL_Libretro_MenuAppendExtensions(filter, sizeof(filter), "zip");
1533#endif
1534
1535 // With nothing to filter against, show every file.
1536 nk_console_file_set_filter(menu->loadGameButton, filter[0] != '\0' ? filter : NULL);
1537#else
1538 (void)menu;
1539#endif
1540}
1541
1546static void SDL_Libretro_MenuSyncSettingsBuffers(SDL_LibretroMenu* menu) {
1547 SDL_Libretro* lr = menu->lr;
1548 const char* value = SDL_Libretro_GetCoreDirectory(lr);
1549 SDL_strlcpy(menu->coreDirBuffer, value != NULL ? value : "", sizeof(menu->coreDirBuffer));
1551 SDL_strlcpy(menu->saveDirBuffer, value != NULL ? value : "", sizeof(menu->saveDirBuffer));
1553 SDL_strlcpy(menu->systemDirBuffer, value != NULL ? value : "", sizeof(menu->systemDirBuffer));
1554 SDL_strlcpy(menu->browseDirBuffer, lr->fileBrowserStartDirectory, sizeof(menu->browseDirBuffer));
1555 value = SDL_Libretro_GetUsername(lr);
1556 SDL_strlcpy(menu->usernameBuffer, value != NULL ? value : "", sizeof(menu->usernameBuffer));
1557}
1558
1566static void SDL_Libretro_MenuSettingChanged(nk_console* widget, void* user_data) {
1567 (void)widget;
1568 SDL_LibretroMenu* menu = (SDL_LibretroMenu*)user_data;
1569 SDL_Libretro_SetCoreDirectory(menu->lr, menu->coreDirBuffer);
1570 SDL_Libretro_MenuUpdateLoadGameFilter(menu);
1571 SDL_Libretro_SetSaveDirectory(menu->lr, menu->saveDirBuffer);
1572 SDL_Libretro_SetSystemDirectory(menu->lr, menu->systemDirBuffer);
1573 SDL_strlcpy(menu->lr->fileBrowserStartDirectory, menu->browseDirBuffer, sizeof(menu->lr->fileBrowserStartDirectory));
1574#ifndef __EMSCRIPTEN__
1575 if (menu->loadGameButton != NULL && menu->browseDirBuffer[0] != '\0') {
1576 nk_console_file_set_directory(menu->loadGameButton, menu->browseDirBuffer);
1577 }
1578#endif
1579 SDL_Libretro_SetUsername(menu->lr, menu->usernameBuffer);
1580}
1581
1587static nk_console* SDL_Libretro_MenuAddSettingTextedit(nk_console* parent, const char* label, SDL_LibretroMenu* menu, char* buffer, size_t bufferSize) {
1588 return SDL_Libretro_MenuOnChanged(nk_console_textedit(parent, label, buffer, bufferSize), &SDL_Libretro_MenuSettingChanged, menu);
1589}
1590
1596static nk_console* SDL_Libretro_MenuAddSettingDir(nk_console* parent, const char* label, SDL_LibretroMenu* menu, char* buffer, size_t bufferSize) {
1597#ifdef __EMSCRIPTEN__
1598 // The web build's virtual file system isn't worth browsing.
1599 return SDL_Libretro_MenuAddSettingTextedit(parent, label, menu, buffer, bufferSize);
1600#else
1601 // nk_console_dir (not dir_action) keeps the label in its own column, so
1602 // the directory type stays visible next to the picked path.
1603 return SDL_Libretro_MenuOnChanged(nk_console_dir(parent, label, buffer, (int)bufferSize), &SDL_Libretro_MenuSettingChanged, menu);
1604#endif
1605}
1606
1610static void SDL_Libretro_MenuFreePortStates(SDL_LibretroMenu* menu) {
1611 for (unsigned i = 0; i < SDL_LIBRETRO_MAX_USERS; i++) {
1612 SDL_free(menu->portStates[i].deviceList);
1613 menu->portStates[i].deviceList = NULL;
1614 }
1615}
1616
1620static void SDL_Libretro_MenuPortDeviceChanged(nk_console* widget, void* user_data) {
1621 (void)widget;
1622 SDL_LibretroMenuPortState* state = (SDL_LibretroMenuPortState*)user_data;
1623 SDL_Libretro* lr = state->menu->lr;
1624 if (state->port >= lr->core.controllerInfoCount) {
1625 return;
1626 }
1627 const struct retro_controller_info* info = &lr->core.controllerInfo[state->port];
1628 if (state->selected < 0 || (unsigned)state->selected >= info->num_types) {
1629 return;
1630 }
1631 SDL_Libretro_SetPortDevice(lr, state->port, info->types[state->selected].id);
1632}
1633
1638static const char* SDL_Libretro_MenuPortDeviceText(void* userdata, unsigned index) {
1639 const struct retro_controller_info* info = (const struct retro_controller_info*)userdata;
1640 return info->types[index].desc != NULL ? info->types[index].desc : "Unknown";
1641}
1642
1648static void SDL_Libretro_MenuBuildControllers(SDL_LibretroMenu* menu) {
1649 if (menu->controllersButton == NULL) {
1650 return;
1651 }
1652 SDL_Libretro* lr = menu->lr;
1653
1654 // Drop the previous widgets before the port states they point into.
1655 SDL_Libretro_MenuGuardActiveParent(menu, menu->controllersButton);
1656 nk_console_free_children(menu->controllersButton);
1657 SDL_Libretro_MenuFreePortStates(menu);
1658
1659 SDL_Libretro_MenuAddBackButton(menu->controllersButton, "Controllers");
1660
1661 unsigned count = lr->core.controllerInfoCount;
1662 if (count > SDL_LIBRETRO_MAX_USERS) {
1663 count = SDL_LIBRETRO_MAX_USERS;
1664 }
1665
1666 bool anyWidget = false;
1667 for (unsigned port = 0; port < count; port++) {
1668 const struct retro_controller_info* info = &lr->core.controllerInfo[port];
1669 if (info->types == NULL || info->num_types <= 1) {
1670 continue;
1671 }
1672
1673 // The combobox reads the pipe-separated list lazily, so keep it alive in the state.
1674 SDL_LibretroMenuPortState* state = &menu->portStates[port];
1675 state->deviceList = SDL_Libretro_MenuJoinPipeList(info->num_types, &SDL_Libretro_MenuPortDeviceText, (void*)info);
1676 if (state->deviceList == NULL) {
1677 continue;
1678 }
1679 state->menu = menu;
1680 state->port = port;
1681 state->selected = 0;
1682
1683 unsigned currentDevice = SDL_Libretro_GetPortDevice(lr, port);
1684 for (unsigned i = 0; i < info->num_types; i++) {
1685 if (info->types[i].id == currentDevice) {
1686 state->selected = (int)i;
1687 }
1688 }
1689
1690 SDL_snprintf(state->label, sizeof(state->label), "Port %u", port + 1);
1691 SDL_Libretro_MenuOnChanged(nk_console_combobox(menu->controllersButton, state->label, state->deviceList, '|', &state->selected), &SDL_Libretro_MenuPortDeviceChanged, state);
1692 anyWidget = true;
1693 }
1694
1695 menu->controllersButton->visible = (nk_bool)anyWidget;
1696}
1697
1705typedef struct SDL_LibretroMenuPalette {
1706 struct nk_color text;
1707 struct nk_color window;
1708 struct nk_color frame;
1709 struct nk_color widget;
1710 struct nk_color widgetHover;
1711 struct nk_color widgetActive;
1712 struct nk_color selectActive;
1713 struct nk_color toggle;
1714 struct nk_color toggleHover;
1715 struct nk_color toggleCursor;
1716 struct nk_color slider;
1717 struct nk_color sliderCursor;
1718 struct nk_color sliderCursorHover;
1719 struct nk_color sliderCursorActive;
1720 struct nk_color editCursor;
1721 struct nk_color chartColor;
1722 struct nk_color chartHighlight;
1723 struct nk_color scrollbar;
1724 struct nk_color scrollbarCursor;
1725 struct nk_color scrollbarCursorHover;
1726 struct nk_color scrollbarCursorActive;
1727 struct nk_color knobCursor;
1728 struct nk_color knobCursorHover;
1729 struct nk_color knobCursorActive;
1730} SDL_LibretroMenuPalette;
1731
1732// Catppuccin Mocha: text/base/mantle/surface/overlay neutrals with lavender,
1733// green and pink accents. https://catppuccin.com/palette/
1734static const SDL_LibretroMenuPalette SDL_Libretro_menuPaletteMocha = {
1735 .text = {205, 214, 244, 255},
1736 .window = {30, 30, 46, 235},
1737 .frame = {24, 24, 37, 255},
1738 .widget = {49, 50, 68, 255},
1739 .widgetHover = {127, 132, 156, 255},
1740 .widgetActive = {108, 112, 134, 255},
1741 .selectActive = {108, 112, 134, 255},
1742 .toggle = {88, 91, 112, 255},
1743 .toggleHover = {147, 153, 178, 255},
1744 .toggleCursor = {180, 190, 254, 255},
1745 .slider = {69, 71, 90, 255},
1746 .sliderCursor = {166, 227, 161, 255},
1747 .sliderCursorHover = {166, 227, 161, 255},
1748 .sliderCursorActive = {166, 227, 161, 255},
1749 .editCursor = {180, 190, 254, 255},
1750 .chartColor = {180, 190, 254, 255},
1751 .chartHighlight = {245, 194, 231, 255},
1752 .scrollbar = {49, 50, 68, 255},
1753 .scrollbarCursor = {108, 112, 134, 255},
1754 .scrollbarCursorHover = {180, 190, 254, 255},
1755 .scrollbarCursorActive = {245, 194, 231, 255},
1756 .knobCursor = {245, 194, 231, 255},
1757 .knobCursorHover = {245, 194, 231, 255},
1758 .knobCursorActive = {245, 194, 231, 255},
1759};
1760
1761// Catppuccin Latte: the light variant, with teal, mauve and pink accents.
1762static const SDL_LibretroMenuPalette SDL_Libretro_menuPaletteLatte = {
1763 .text = {76, 79, 105, 255},
1764 .window = {239, 241, 245, 235},
1765 .frame = {230, 233, 239, 255},
1766 .widget = {204, 208, 218, 255},
1767 .widgetHover = {124, 127, 147, 55},
1768 .widgetActive = {156, 160, 176, 255},
1769 .selectActive = {156, 160, 176, 255},
1770 .toggle = {172, 176, 190, 255},
1771 .toggleHover = {124, 127, 147, 55},
1772 .toggleCursor = {223, 142, 29, 255},
1773 .slider = {188, 192, 204, 255},
1774 .sliderCursor = {23, 146, 153, 255},
1775 .sliderCursorHover = {23, 146, 153, 255},
1776 .sliderCursorActive = {23, 146, 153, 255},
1777 .editCursor = {136, 57, 239, 255},
1778 .chartColor = {23, 146, 153, 255},
1779 .chartHighlight = {136, 57, 239, 255},
1780 .scrollbar = {204, 208, 218, 255},
1781 .scrollbarCursor = {156, 160, 176, 255},
1782 .scrollbarCursorHover = {136, 57, 239, 255},
1783 .scrollbarCursorActive = {136, 57, 239, 255},
1784 .knobCursor = {234, 118, 203, 255},
1785 .knobCursorHover = {234, 118, 203, 255},
1786 .knobCursorActive = {234, 118, 203, 255},
1787};
1788
1789// Catppuccin Frappe, with green, lavender and pink accents.
1790static const SDL_LibretroMenuPalette SDL_Libretro_menuPaletteFrappe = {
1791 .text = {198, 208, 245, 255},
1792 .window = {48, 52, 70, 235},
1793 .frame = {41, 44, 60, 255},
1794 .widget = {65, 69, 89, 255},
1795 .widgetHover = {131, 139, 167, 255},
1796 .widgetActive = {115, 121, 148, 255},
1797 .selectActive = {115, 121, 148, 255},
1798 .toggle = {98, 104, 128, 255},
1799 .toggleHover = {148, 156, 187, 255},
1800 .toggleCursor = {244, 184, 228, 255},
1801 .slider = {81, 87, 109, 255},
1802 .sliderCursor = {166, 209, 137, 255},
1803 .sliderCursorHover = {166, 209, 137, 255},
1804 .sliderCursorActive = {166, 209, 137, 255},
1805 .editCursor = {244, 184, 228, 255},
1806 .chartColor = {186, 187, 241, 255},
1807 .chartHighlight = {244, 184, 228, 255},
1808 .scrollbar = {65, 69, 89, 255},
1809 .scrollbarCursor = {115, 121, 148, 255},
1810 .scrollbarCursorHover = {186, 187, 241, 255},
1811 .scrollbarCursorActive = {186, 187, 241, 255},
1812 .knobCursor = {244, 184, 228, 255},
1813 .knobCursorHover = {244, 184, 228, 255},
1814 .knobCursorActive = {244, 184, 228, 255},
1815};
1816
1817// Catppuccin Macchiato, with yellow, green, lavender and pink accents.
1818static const SDL_LibretroMenuPalette SDL_Libretro_menuPaletteMacchiato = {
1819 .text = {202, 211, 245, 255},
1820 .window = {36, 39, 58, 235},
1821 .frame = {30, 32, 48, 255},
1822 .widget = {54, 58, 79, 255},
1823 .widgetHover = {128, 135, 162, 255},
1824 .widgetActive = {110, 115, 141, 255},
1825 .selectActive = {110, 115, 141, 255},
1826 .toggle = {91, 96, 120, 255},
1827 .toggleHover = {147, 154, 183, 255},
1828 .toggleCursor = {238, 212, 159, 255},
1829 .slider = {73, 77, 100, 255},
1830 .sliderCursor = {166, 218, 149, 255},
1831 .sliderCursorHover = {166, 218, 149, 255},
1832 .sliderCursorActive = {166, 218, 149, 255},
1833 .editCursor = {245, 189, 230, 255},
1834 .chartColor = {183, 189, 248, 255},
1835 .chartHighlight = {238, 212, 159, 255},
1836 .scrollbar = {54, 58, 79, 255},
1837 .scrollbarCursor = {110, 115, 141, 255},
1838 .scrollbarCursorHover = {183, 189, 248, 255},
1839 .scrollbarCursorActive = {183, 189, 248, 255},
1840 .knobCursor = {245, 189, 230, 255},
1841 .knobCursorHover = {245, 189, 230, 255},
1842 .knobCursorActive = {245, 189, 230, 255},
1843};
1844
1845// Dracula: background/current-line/comment neutrals with pink and purple
1846// accents. https://draculatheme.com/contribute
1847static const SDL_LibretroMenuPalette SDL_Libretro_menuPaletteDracula = {
1848 .text = {248, 248, 242, 255},
1849 .window = {40, 42, 54, 235},
1850 .frame = {68, 71, 90, 255},
1851 .widget = {68, 71, 90, 255},
1852 .widgetHover = {98, 114, 164, 255},
1853 .widgetActive = {189, 147, 249, 255},
1854 .selectActive = {98, 114, 164, 255},
1855 .toggle = {68, 71, 90, 255},
1856 .toggleHover = {98, 114, 164, 255},
1857 .toggleCursor = {255, 121, 198, 255},
1858 .slider = {40, 42, 54, 235},
1859 .sliderCursor = {68, 71, 90, 255},
1860 .sliderCursorHover = {98, 114, 164, 255},
1861 .sliderCursorActive = {98, 114, 164, 255},
1862 .editCursor = {248, 248, 242, 255},
1863 .chartColor = {98, 114, 164, 255},
1864 .chartHighlight = {189, 147, 249, 255},
1865 .scrollbar = {40, 42, 54, 235},
1866 .scrollbarCursor = {68, 71, 90, 255},
1867 .scrollbarCursorHover = {98, 114, 164, 255},
1868 .scrollbarCursorActive = {189, 147, 249, 255},
1869 .knobCursor = {68, 71, 90, 255},
1870 .knobCursorHover = {98, 114, 164, 255},
1871 .knobCursorActive = {98, 114, 164, 255},
1872};
1873
1878static const SDL_LibretroMenuPalette* SDL_Libretro_MenuGetPalette(SDL_LibretroMenuStyle style) {
1879 static const SDL_LibretroMenuPalette* const palettes[SDL_LIBRETRO_MENU_STYLE_COUNT] = {
1880 &SDL_Libretro_menuPaletteMocha,
1881 &SDL_Libretro_menuPaletteLatte,
1882 &SDL_Libretro_menuPaletteFrappe,
1883 &SDL_Libretro_menuPaletteMacchiato,
1884 &SDL_Libretro_menuPaletteDracula,
1885 NULL, // SDL_LIBRETRO_MENU_STYLE_DARK is the Nuklear default.
1886 };
1887 return ((int)style >= 0 && style < SDL_LIBRETRO_MENU_STYLE_COUNT) ? palettes[style] : NULL;
1888}
1889
1896static float SDL_Libretro_MenuAutoScale(int width) {
1897 if (width >= 3840) {
1898 return 4.0f;
1899 }
1900 if (width >= 2560) {
1901 return 3.0f;
1902 }
1903 if (width >= 1280) {
1904 return 2.0f;
1905 }
1906 return 1.0f;
1907}
1908
1914static float SDL_Libretro_MenuRenderScale(SDL_LibretroMenu* menu) {
1915 float displayScale = SDL_GetWindowDisplayScale(menu->lr->window);
1916 int scale = (displayScale > 0.0f) ? (int)(displayScale + 0.5f) : 1;
1917 if (scale < 1) {
1918 scale = 1;
1919 }
1920 int step;
1921 if (menu->uiScaleIndex > 0) {
1922 step = menu->uiScaleIndex;
1923 }
1924 else {
1925 int width = 0;
1926 int height = 0;
1927 SDL_GetWindowSize(menu->lr->window, &width, &height);
1928 step = (int)SDL_Libretro_MenuAutoScale(width);
1929 }
1930 return (float)(step * scale);
1931}
1932
1939static void SDL_Libretro_MenuBakeFont(SDL_LibretroMenu* menu) {
1940 if (menu->atlas != NULL) {
1941 nk_font_atlas_clear(menu->atlas);
1942 menu->atlas = NULL;
1943 }
1944 struct nk_sdl* sdl = (struct nk_sdl*)menu->ctx->userdata.ptr;
1945 struct nk_font_config fontConfig = nk_font_config(0);
1946 fontConfig.oversample_h = 1;
1947 fontConfig.oversample_v = 1;
1948 fontConfig.pixel_snap = nk_true;
1949 menu->atlas = nk_sdl_font_stash_begin(menu->ctx);
1950 struct nk_font* font = nk_font_atlas_add_default(menu->atlas, (float)SDL_LIBRETRO_MENU_FONT_HEIGHT, &fontConfig);
1951
1952 int atlasWidth = 0, atlasHeight = 0;
1953 const void* fontImage = nk_font_atlas_bake(menu->atlas, &atlasWidth, &atlasHeight, NK_FONT_ATLAS_RGBA32);
1954 if (fontImage != NULL) {
1955 Uint8* texels = (Uint8*)fontImage;
1956 for (int i = 3; i < atlasWidth * atlasHeight * 4; i += 4) {
1957 texels[i] = texels[i] >= 128 ? 255 : 0;
1958 }
1959 }
1960 nk_sdl_device_upload_atlas(menu->ctx, fontImage, atlasWidth, atlasHeight);
1961 nk_font_atlas_end(menu->atlas, nk_handle_ptr(sdl->ogl.font_tex), &sdl->ogl.tex_null);
1962 if (sdl->ogl.font_tex != NULL) {
1963 SDL_SetTextureScaleMode(sdl->ogl.font_tex, SDL_SCALEMODE_NEAREST);
1964 }
1965 if (font != NULL) {
1966 nk_style_set_font(menu->ctx, &font->handle);
1967 }
1968}
1969
1974static void SDL_Libretro_MenuApplyPalette(struct nk_context* ctx, const SDL_LibretroMenuPalette* palette) {
1975 struct nk_color table[NK_COLOR_COUNT];
1976 table[NK_COLOR_TEXT] = palette->text;
1977 table[NK_COLOR_WINDOW] = palette->window;
1978 table[NK_COLOR_HEADER] = palette->frame;
1979 table[NK_COLOR_BORDER] = palette->frame;
1980 table[NK_COLOR_BUTTON] = palette->widget;
1981 table[NK_COLOR_BUTTON_HOVER] = palette->widgetHover;
1982 table[NK_COLOR_BUTTON_ACTIVE] = palette->widgetActive;
1983 table[NK_COLOR_TOGGLE] = palette->toggle;
1984 table[NK_COLOR_TOGGLE_HOVER] = palette->toggleHover;
1985 table[NK_COLOR_TOGGLE_CURSOR] = palette->toggleCursor;
1986 table[NK_COLOR_SELECT] = palette->widget;
1987 table[NK_COLOR_SELECT_ACTIVE] = palette->selectActive;
1988 table[NK_COLOR_SLIDER] = palette->slider;
1989 table[NK_COLOR_SLIDER_CURSOR] = palette->sliderCursor;
1990 table[NK_COLOR_SLIDER_CURSOR_HOVER] = palette->sliderCursorHover;
1991 table[NK_COLOR_SLIDER_CURSOR_ACTIVE] = palette->sliderCursorActive;
1992 table[NK_COLOR_PROPERTY] = palette->widget;
1993 table[NK_COLOR_EDIT] = palette->widget;
1994 table[NK_COLOR_EDIT_CURSOR] = palette->editCursor;
1995 table[NK_COLOR_COMBO] = palette->widget;
1996 table[NK_COLOR_CHART] = palette->widget;
1997 table[NK_COLOR_CHART_COLOR] = palette->chartColor;
1998 table[NK_COLOR_CHART_COLOR_HIGHLIGHT] = palette->chartHighlight;
1999 table[NK_COLOR_SCROLLBAR] = palette->scrollbar;
2000 table[NK_COLOR_SCROLLBAR_CURSOR] = palette->scrollbarCursor;
2001 table[NK_COLOR_SCROLLBAR_CURSOR_HOVER] = palette->scrollbarCursorHover;
2002 table[NK_COLOR_SCROLLBAR_CURSOR_ACTIVE] = palette->scrollbarCursorActive;
2003 table[NK_COLOR_TAB_HEADER] = palette->widget;
2004 table[NK_COLOR_KNOB] = palette->slider;
2005 table[NK_COLOR_KNOB_CURSOR] = palette->knobCursor;
2006 table[NK_COLOR_KNOB_CURSOR_HOVER] = palette->knobCursorHover;
2007 table[NK_COLOR_KNOB_CURSOR_ACTIVE] = palette->knobCursorActive;
2008 nk_style_from_table(ctx, table);
2009}
2010
2014static void SDL_Libretro_MenuUIScaleChanged(nk_console* widget, void* user_data) {
2015 (void)widget;
2016 SDL_LibretroMenu* menu = (SDL_LibretroMenu*)user_data;
2017 menu->settingsDirty = true;
2018}
2019
2034bool SDL_Libretro_SetMenuStyle(SDL_LibretroMenu* menu, SDL_LibretroMenuStyle style) {
2035 if (menu == NULL || menu->ctx == NULL || (int)style < 0 || style >= SDL_LIBRETRO_MENU_STYLE_COUNT) {
2036 return false;
2037 }
2038
2039 // Reset to the Nuklear default first; the Dark style is exactly that.
2040 nk_style_default(menu->ctx);
2041 const SDL_LibretroMenuPalette* palette = SDL_Libretro_MenuGetPalette(style);
2042 if (palette != NULL) {
2043 SDL_Libretro_MenuApplyPalette(menu->ctx, palette);
2044 }
2045
2046 menu->style = style;
2047 menu->styleIndex = (int)style;
2048 menu->settingsDirty = true;
2049 return true;
2050}
2051
2062 return menu != NULL ? menu->style : SDL_LIBRETRO_MENU_DEFAULT_STYLE;
2063}
2064
2068static void SDL_Libretro_MenuStyleChanged(nk_console* widget, void* user_data) {
2069 (void)widget;
2070 SDL_LibretroMenu* menu = (SDL_LibretroMenu*)user_data;
2071 SDL_Libretro_SetMenuStyle(menu, (SDL_LibretroMenuStyle)menu->styleIndex);
2072}
2073
2081static void SDL_Libretro_MenuSyncSettings(SDL_LibretroMenu* menu) {
2082 SDL_Libretro* lr = menu->lr;
2083 menu->volumePercent = (int)(SDL_Libretro_GetVolume(lr) * 100.0f + 0.5f);
2084 menu->fitModeIndex = (int)SDL_Libretro_GetFitMode(lr);
2085 menu->filterIndex = SDL_Libretro_GetScaleMode(lr) == SDL_SCALEMODE_LINEAR ? 1 : 0;
2086 menu->fullscreenChecked = (nk_bool)((SDL_GetWindowFlags(lr->window) & SDL_WINDOW_FULLSCREEN) != 0);
2087 int vsync = 0;
2088 SDL_GetRenderVSync(lr->renderer, &vsync);
2089 menu->vsyncChecked = (nk_bool)(vsync != 0);
2090 menu->rewindChecked = (nk_bool)SDL_Libretro_GetRewindEnabled(lr);
2091 // Clamped to the widget's range: nk_property clamps the bound value while
2092 // rendering, and the CHANGED writeback would shrink a larger app budget.
2093 size_t rewindLimitMB = SDL_Libretro_GetRewindMemoryLimit(lr) >> 20;
2094 menu->rewindBufferMB = rewindLimitMB > 1024 ? 1024 : (int)rewindLimitMB;
2095 menu->mousePortIndex = (int)SDL_Libretro_GetMousePort(lr);
2096 // The raw override, so a locale-detected language still shows as Auto.
2097 menu->languageIndex = lr->language >= 0 ? lr->language + 1 : 0;
2098}
2099
2104static void SDL_Libretro_MenuDiskClicked(nk_console* widget, void* user_data) {
2105 (void)widget;
2106 SDL_LibretroMenu* menu = (SDL_LibretroMenu*)user_data;
2107 SDL_Libretro* lr = menu->lr;
2108 if (menu->diskSelected < 0 || (unsigned)menu->diskSelected == SDL_Libretro_GetDiskIndex(lr)) {
2109 return;
2110 }
2111 bool changed = false;
2112 if (SDL_Libretro_EjectDisk(lr)) {
2113 changed = SDL_Libretro_SetDiskIndex(lr, (unsigned)menu->diskSelected);
2114 // Close the tray even when the index change failed.
2115 changed = SDL_Libretro_InsertDisk(lr) && changed;
2116 }
2117 if (changed) {
2118 nk_console_show_message(menu->console, "Disk changed");
2119 }
2120 else {
2121 nk_console_show_message(menu->console, "Disk change failed");
2122 menu->diskSelected = (int)SDL_Libretro_GetDiskIndex(lr);
2123 }
2124}
2125
2129static void SDL_Libretro_MenuFreeDiskLabels(SDL_LibretroMenu* menu) {
2130 if (menu->diskLabels == NULL) {
2131 return;
2132 }
2133 for (unsigned i = 0; i < menu->builtDiskCount; i++) {
2134 SDL_free(menu->diskLabels[i]);
2135 }
2136 SDL_free(menu->diskLabels);
2137 menu->diskLabels = NULL;
2138}
2139
2145static void SDL_Libretro_MenuBuildDisks(SDL_LibretroMenu* menu) {
2146 if (menu->disksButton == NULL) {
2147 return;
2148 }
2149 SDL_Libretro* lr = menu->lr;
2150
2151 // Drop the previous widgets before the labels they point into.
2152 SDL_Libretro_MenuGuardActiveParent(menu, menu->disksButton);
2153 nk_console_free_children(menu->disksButton);
2154 SDL_Libretro_MenuFreeDiskLabels(menu);
2155
2156 unsigned count = SDL_Libretro_GetDiskCount(lr);
2157 menu->builtDiskCount = count;
2158 if (count < 2) {
2159 // The page is going away; don't strand the user on an empty parent.
2160 if (nk_console_active_parent(menu->console) == menu->disksButton) {
2161 nk_console_set_active_parent(menu->console);
2162 }
2163 menu->disksButton->visible = nk_false;
2164 return;
2165 }
2166
2167 menu->diskLabels = (char**)SDL_calloc(count, sizeof(char*));
2168 if (menu->diskLabels == NULL) {
2169 menu->disksButton->visible = nk_false;
2170 return;
2171 }
2172
2173 menu->disksButton->visible = nk_true;
2174 SDL_Libretro_MenuAddBackButton(menu->disksButton, "Disks");
2175 menu->diskSelected = (int)SDL_Libretro_GetDiskIndex(lr);
2176 for (unsigned i = 0; i < count; i++) {
2177 char label[128];
2178 if (!SDL_Libretro_GetDiskLabel(lr, i, label, sizeof(label)) || label[0] == '\0') {
2179 SDL_snprintf(label, sizeof(label), "Disk %u", i + 1);
2180 }
2181 menu->diskLabels[i] = SDL_strdup(label);
2182 nk_console* radio = nk_console_radio(menu->disksButton, menu->diskLabels[i] != NULL ? menu->diskLabels[i] : "Disk", &menu->diskSelected);
2183 // Radios report a pick as CLICKED.
2184 nk_console_add_event_handler(radio, NK_CONSOLE_EVENT_CLICKED, &SDL_Libretro_MenuDiskClicked, menu, NULL);
2185 }
2186}
2187
2193static void SDL_Libretro_MenuBuildSettings(SDL_LibretroMenu* menu) {
2194 nk_console* settings = SDL_Libretro_MenuAddSubmenu(menu->console, "Settings", NK_SYMBOL_HAMBURGER);
2195
2196 // General
2197 nk_console* general = SDL_Libretro_MenuAddSubmenu(settings, "General", NK_SYMBOL_TRIANGLE_RIGHT);
2198 SDL_Libretro_MenuSyncSettingsBuffers(menu);
2199 nk_console_set_tooltip(
2200 SDL_Libretro_MenuAddSettingTextedit(general, "Username", menu, menu->usernameBuffer, sizeof(menu->usernameBuffer)),
2201 "Reported to cores that ask for a username");
2202
2203 // Rewind toggle, with its memory budget below it.
2204 nk_console_set_tooltip(
2205 SDL_Libretro_MenuAddCheckbox(menu, general, "Rewind", &menu->rewindChecked, NULL, &SDL_Libretro_MenuRewindChanged, NULL),
2206 "Capture state snapshots so gameplay can rewind");
2207 nk_console_set_tooltip(
2208 SDL_Libretro_MenuOnChanged(nk_console_property_int(general, "Rewind Buffer (MB)", 0, &menu->rewindBufferMB, 1024, 8, 1.0f), &SDL_Libretro_MenuRewindBufferChanged, menu),
2209 "Memory the rewind buffer may use; 0 for no limit");
2210 nk_console_set_tooltip(
2211 SDL_Libretro_MenuOnChanged(nk_console_combobox(general, "Language", SDL_LIBRETRO_MENU_LANGUAGE_NAMES, '|', &menu->languageIndex), &SDL_Libretro_MenuLanguageChanged, menu),
2212 "Reported to cores; applies when the next core loads. Auto follows the system locale");
2213
2214 // Audio & Video
2215 nk_console* audioVideo = SDL_Libretro_MenuAddSubmenu(settings, "Audio & Video", NK_SYMBOL_TRIANGLE_RIGHT);
2216
2217 SDL_Libretro_MenuOnChanged(nk_console_knob_int(audioVideo, "Volume", 0, &menu->volumePercent, 100, 5, 1.0f), &SDL_Libretro_MenuVolumeChanged, menu);
2218 nk_console_set_tooltip(
2219 SDL_Libretro_MenuAddCheckbox(menu, audioVideo, "Mute", &menu->muteChecked, NULL, &SDL_Libretro_MenuMuteChanged, NULL),
2220 "Silence the audio; unmuting restores the volume");
2221 SDL_Libretro_MenuAddCheckbox(menu, audioVideo, "Fullscreen", &menu->fullscreenChecked, NULL, &SDL_Libretro_MenuFullscreenChanged, NULL);
2222 SDL_Libretro_MenuAddCheckbox(menu, audioVideo, "VSync", &menu->vsyncChecked, NULL, &SDL_Libretro_MenuVSyncChanged, NULL);
2223 nk_console_set_tooltip(
2224 SDL_Libretro_MenuOnChanged(nk_console_combobox(audioVideo, "Filter", "Nearest|Linear", '|', &menu->filterIndex), &SDL_Libretro_MenuFilterChanged, menu),
2225 "Nearest keeps pixels sharp; Linear smooths the scaling");
2226 nk_console_set_tooltip(
2227 SDL_Libretro_MenuOnChanged(nk_console_combobox(audioVideo, "Fit Mode", "Aspect|Integer|Stretch", '|', &menu->fitModeIndex), &SDL_Libretro_MenuFitModeChanged, menu),
2228 "Aspect keeps the ratio; Integer locks whole-number scales; Stretch fills the window");
2229 SDL_Libretro_MenuOnChanged(nk_console_combobox(audioVideo, "Theme", SDL_LIBRETRO_MENU_STYLE_NAMES, '|', &menu->styleIndex), &SDL_Libretro_MenuStyleChanged, menu);
2230 nk_console_set_tooltip(
2231 SDL_Libretro_MenuOnChanged(nk_console_combobox(audioVideo, "UI Scale", "Auto|1x|2x|3x|4x", '|', &menu->uiScaleIndex), &SDL_Libretro_MenuUIScaleChanged, menu),
2232 "Menu size; Auto picks one from the resolution");
2233
2234 // Core Options, populated lazily once a core registers options.
2235 menu->optionsButton = nk_console_button(settings, "Core Options");
2236 nk_console_button_set_symbol(menu->optionsButton, NK_SYMBOL_TRIANGLE_RIGHT);
2237 menu->optionsButton->visible = nk_false;
2238
2239 // Controllers, populated lazily once a core registers controller info.
2240 menu->controllersButton = nk_console_button(settings, "Controllers");
2241 nk_console_button_set_symbol(menu->controllersButton, NK_SYMBOL_TRIANGLE_RIGHT);
2242 menu->controllersButton->visible = nk_false;
2243
2244 // Keyboard bindings for player 1's virtual controller.
2245 nk_console* keyboard = SDL_Libretro_MenuAddSubmenu(settings, "Keyboard", NK_SYMBOL_TRIANGLE_RIGHT);
2246 nk_console_set_tooltip(keyboard, "Keyboard keys for Player 1's controller, and the mouse's port");
2247 SDL_Libretro_MenuSyncKeyBinds(menu);
2248
2249 // The port the mouse, pointer and lightgun answer on.
2250 menu->mousePortList[0] = '\0';
2251 for (unsigned port = 0; port < SDL_LIBRETRO_MAX_USERS; port++) {
2252 char item[16];
2253 SDL_snprintf(item, sizeof(item), port == 0 ? "Port %u" : "|Port %u", port + 1);
2254 SDL_strlcat(menu->mousePortList, item, sizeof(menu->mousePortList));
2255 }
2256 nk_console_set_tooltip(
2257 SDL_Libretro_MenuOnChanged(nk_console_combobox(keyboard, "Mouse Port", menu->mousePortList, '|', &menu->mousePortIndex), &SDL_Libretro_MenuMousePortChanged, menu),
2258 "The controller port the mouse, pointer and lightgun drive");
2259
2260 for (int i = 0; i < SDL_LIBRETRO_MAX_JOYPAD_BUTTONS; i++) {
2261 menu->keyBinds[i].menu = menu;
2262 menu->keyBinds[i].button = i;
2263 SDL_Libretro_MenuOnChanged(
2264 nk_console_input_key(keyboard, SDL_Libretro_JoypadButtonNames[i], &menu->keyBinds[i].rune),
2265 &SDL_Libretro_MenuKeyBindChanged,
2266 &menu->keyBinds[i]);
2267 }
2268
2269 // Gamepad bindings and analog-to-digital, one page per player.
2270 menu->gamepadButton = nk_console_button(settings, "Gamepad");
2271 nk_console_button_set_symbol(menu->gamepadButton, NK_SYMBOL_TRIANGLE_RIGHT);
2272 nk_console_set_tooltip(menu->gamepadButton, "Per-player gamepad buttons for the retro controllers");
2273 SDL_Libretro_MenuBuildGamepads(menu);
2274
2275 // Directories
2276 nk_console* directories = SDL_Libretro_MenuAddSubmenu(settings, "Directories", NK_SYMBOL_TRIANGLE_RIGHT);
2277
2278 SDL_Libretro_MenuAddSettingDir(directories, "Cores", menu, menu->coreDirBuffer, sizeof(menu->coreDirBuffer));
2279 SDL_Libretro_MenuAddSettingDir(directories, "Saves", menu, menu->saveDirBuffer, sizeof(menu->saveDirBuffer));
2280 SDL_Libretro_MenuAddSettingDir(directories, "System", menu, menu->systemDirBuffer, sizeof(menu->systemDirBuffer));
2281 SDL_Libretro_MenuAddSettingDir(directories, "Content", menu, menu->browseDirBuffer, sizeof(menu->browseDirBuffer));
2282}
2283
2288static void SDL_Libretro_MenuBuildWidgets(SDL_LibretroMenu* menu) {
2289 SDL_Libretro* lr = menu->lr;
2290 (void)lr;
2291
2292 // Backing out of the top level acts like Resume.
2293 nk_console_add_event_handler(menu->console, NK_CONSOLE_EVENT_BACK, &SDL_Libretro_MenuBackClicked, menu, NULL);
2294
2295 // Progress reported by the core through a progress-type OSD message.
2296 menu->osdProgressWidget = nk_console_progress(menu->console, "Progress", &menu->osdProgressValue, 100);
2297 menu->osdProgressWidget->visible = nk_false;
2298
2299 // Resume
2300 menu->resumeButton = SDL_Libretro_MenuAddButton(menu, menu->console, "Resume", NK_SYMBOL_TRIANGLE_RIGHT, &SDL_Libretro_MenuResumeClicked, NULL);
2301
2302 // Load Game
2303#ifdef __EMSCRIPTEN__
2304 // Browsing the virtual file system isn't useful on the web; a JS file
2305 // picker brings the user's own files in instead.
2306 menu->loadGameButton = SDL_Libretro_MenuAddButton(menu, menu->console, "Load Game", NK_SYMBOL_NONE, &SDL_Libretro_MenuWebLoadGameClicked, NULL);
2307#else
2308 menu->loadGameButton = SDL_Libretro_MenuOnChanged(
2309 nk_console_file_action(menu->console, "Load Game", menu->loadGamePath, sizeof(menu->loadGamePath)),
2310 &SDL_Libretro_MenuGameFileChanged,
2311 menu);
2312 if (lr->fileBrowserStartDirectory[0] != '\0') {
2313 nk_console_file_set_directory(menu->loadGameButton, lr->fileBrowserStartDirectory);
2314 }
2315 SDL_Libretro_MenuUpdateLoadGameFilter(menu);
2316#endif
2317
2318 // Save State / Load State
2319 menu->stateRow = nk_console_row_begin(menu->console);
2320 {
2321 SDL_Libretro_MenuAddButton(menu, menu->stateRow, "Save State", NK_SYMBOL_RECT_SOLID, &SDL_Libretro_MenuSaveStateClicked, NULL);
2322 SDL_Libretro_MenuAddButton(menu, menu->stateRow, "Load State", NK_SYMBOL_RECT_OUTLINE, &SDL_Libretro_MenuLoadStateClicked, NULL);
2323 }
2324 nk_console_row_end(menu->stateRow);
2325
2326 // Screenshot
2327 menu->screenshotButton = SDL_Libretro_MenuAddButton(menu, menu->console, "Screenshot", NK_SYMBOL_CIRCLE_OUTLINE, &SDL_Libretro_MenuScreenshotClicked, NULL);
2328
2329 // Reset
2330 menu->resetButton = SDL_Libretro_MenuAddButton(menu, menu->console, "Reset", NK_SYMBOL_CIRCLE_SOLID, &SDL_Libretro_MenuResetClicked, NULL);
2331
2332 // Disks, populated lazily for cores with multi-disk games.
2333 menu->disksButton = SDL_Libretro_MenuAddSubmenu(menu->console, "Disks", NK_SYMBOL_CIRCLE_OUTLINE);
2334 nk_console_set_tooltip(menu->disksButton, "Swap disks for multi-disk games");
2335 menu->disksButton->visible = nk_false;
2336
2337 SDL_Libretro_MenuBuildSettings(menu);
2338
2339 // About, rebuilt by the CLICKED handler whenever the page opens.
2340 menu->aboutButton = SDL_Libretro_MenuAddButton(menu, menu->console, "About", NK_SYMBOL_TRIANGLE_RIGHT, &SDL_Libretro_MenuAboutOpened, NULL);
2341
2342 // Quit, kept as the last top-level entry when the app adds its own.
2343 menu->quitButton = SDL_Libretro_MenuAddButton(menu, menu->console, "Quit", NK_SYMBOL_X, &SDL_Libretro_MenuQuitClicked, NULL);
2344}
2345
2370 if (lr == NULL) {
2371 SDL_InvalidParamError("lr");
2372 return NULL;
2373 }
2374 if (lr->renderer == NULL || lr->window == NULL) {
2375 SDL_SetError("SDL_Libretro_CreateMenu() requires SDL_Libretro_SetRenderer() to be called first");
2376 return NULL;
2377 }
2378
2379 SDL_LibretroMenu* menu = (SDL_LibretroMenu*)SDL_calloc(1, sizeof(SDL_LibretroMenu));
2380 if (menu == NULL) {
2381 return NULL;
2382 }
2383 menu->lr = lr;
2384
2385 menu->ctx = nk_sdl_init(lr->window, lr->renderer, nk_sdl_allocator());
2386 if (menu->ctx == NULL) {
2387 SDL_free(menu);
2388 SDL_SetError("Failed to initialize Nuklear");
2389 return NULL;
2390 }
2391
2392 // Bake the default font once at the base height; UI scaling happens
2393 // through SDL_SetRenderScale in the render path.
2394 SDL_Libretro_MenuBakeFont(menu);
2395
2396 menu->console = nk_console_init(menu->ctx);
2397 if (menu->console == NULL) {
2398 nk_sdl_shutdown(menu->ctx);
2399 SDL_free(menu);
2400 SDL_SetError("Failed to initialize nuklear_console");
2401 return NULL;
2402 }
2403
2404 nk_gamepad_init(&menu->gamepads, menu->ctx, NULL);
2405 nk_console_set_gamepads(menu->console, &menu->gamepads);
2406
2407 SDL_Libretro_MenuLoadState(menu);
2408 menu->renderScale = SDL_Libretro_MenuRenderScale(menu);
2409 SDL_Libretro_MenuSyncSettings(menu);
2410 SDL_Libretro_MenuBuildWidgets(menu);
2411
2412 nk_input_begin(menu->ctx);
2413
2414 return menu;
2415}
2416
2431void SDL_Libretro_DestroyMenu(SDL_LibretroMenu* menu) {
2432 if (menu == NULL) {
2433 return;
2434 }
2435 // Queued menu events carry this menu in data2; drop them before it goes away.
2436 SDL_FlushEvent(SDL_EVENT_LIBRETRO_MENU_OPENED);
2437 SDL_FlushEvent(SDL_EVENT_LIBRETRO_MENU_CLOSED);
2438 if (menu->settingsDirty) {
2439 SDL_Libretro_MenuSaveState(menu);
2440 }
2441 if (menu->ctx != NULL) {
2442 nk_input_end(menu->ctx);
2443 }
2444 if (menu->console != NULL) {
2445 nk_console_free(menu->console);
2446 }
2447 SDL_Libretro_MenuFreeOptionStates(menu);
2448 SDL_Libretro_MenuFreePortStates(menu);
2449 SDL_Libretro_MenuFreeDiskLabels(menu);
2450 nk_gamepad_free(&menu->gamepads);
2451 if (menu->ctx != NULL) {
2452 nk_sdl_shutdown(menu->ctx);
2453 }
2454 SDL_free(menu);
2455}
2456
2470bool SDL_Libretro_IsMenuOpen(const SDL_LibretroMenu* menu) {
2471 return menu != NULL && menu->open;
2472}
2473
2485SDL_Libretro* SDL_Libretro_GetMenuLibretro(const SDL_LibretroMenu* menu) {
2486 return menu != NULL ? menu->lr : NULL;
2487}
2488
2499void SDL_Libretro_SetMenuUserData(SDL_LibretroMenu* menu, void* userData) {
2500 if (menu != NULL) {
2501 menu->userData = userData;
2502 }
2503}
2504
2513void* SDL_Libretro_GetMenuUserData(const SDL_LibretroMenu* menu) {
2514 return menu != NULL ? menu->userData : NULL;
2515}
2516
2532void SDL_Libretro_SetMenuOpen(SDL_LibretroMenu* menu, bool open) {
2533 if (menu == NULL || menu->open == open) {
2534 return;
2535 }
2536 menu->open = open;
2537
2538 // Input held while the menu opens belongs to whatever saw it pressed.
2539 // The application and core still get the key releases, so nothing like
2540 // a held fast-forward key sticks, and the menu doesn't take a held
2541 // gamepad button's release as a click.
2542 if (open) {
2543 int numKeys = 0;
2544 const bool* keys = SDL_GetKeyboardState(&numKeys);
2545 for (int i = 0; i < SDL_SCANCODE_COUNT; i++) {
2546 menu->keysHeldAtOpen[i] = keys != NULL && i < numKeys && keys[i];
2547 }
2548 // Buttons still hidden from an earlier open are held too.
2549 for (int i = 0; i < NK_GAMEPAD_MAX; i++) {
2550 menu->padButtonsHeldAtOpen[i] |= menu->gamepads.gamepads[i].buttons;
2551 }
2552 }
2553 if (!open && menu->lr != NULL && menu->lr->window != NULL) {
2554 SDL_StopTextInput(menu->lr->window);
2555 }
2556 SDL_Libretro_PushEvent(menu->lr, open ? SDL_EVENT_LIBRETRO_MENU_OPENED : SDL_EVENT_LIBRETRO_MENU_CLOSED, menu);
2557}
2558
2569void SDL_Libretro_ToggleMenu(SDL_LibretroMenu* menu) {
2570 if (menu != NULL) {
2571 SDL_Libretro_SetMenuOpen(menu, !menu->open);
2572 }
2573}
2574
2598bool SDL_Libretro_HandleMenuEvent(SDL_LibretroMenu* menu, const SDL_Event* event) {
2599 if (menu == NULL || menu->ctx == NULL || event == NULL) {
2600 return false;
2601 }
2602
2603 // Gamepad connection bookkeeping runs even while the menu is closed.
2604 nk_gamepad_sdl3_handle_event(&menu->gamepads, (SDL_Event*)event);
2605
2606 // Toggle on the menu key or the gamepad Guide button.
2607 // Both edges of the toggle key are consumed so the core never sees an
2608 // unmatched press; only the release toggles.
2609 if ((event->type == SDL_EVENT_KEY_DOWN || event->type == SDL_EVENT_KEY_UP) &&
2610 event->key.key == SDL_LIBRETRO_MENU_TOGGLE_KEY) {
2611 if (event->type == SDL_EVENT_KEY_UP) {
2613 }
2614 return true;
2615 }
2616 if (event->type == SDL_EVENT_GAMEPAD_BUTTON_UP && event->gbutton.button == SDL_GAMEPAD_BUTTON_GUIDE) {
2618 return true;
2619 }
2620
2621 if (!menu->open) {
2622 return false;
2623 }
2624
2625 // Application events, including the menu's own notifications, are never
2626 // swallowed and mean nothing to the UI.
2627 if (event->type >= SDL_EVENT_USER) {
2628 return false;
2629 }
2630
2631 // A key held since before the menu opened releases to the application and
2632 // core, which saw it pressed. A fresh press makes it the menu's.
2633 if ((event->type == SDL_EVENT_KEY_DOWN || event->type == SDL_EVENT_KEY_UP) &&
2634 event->key.scancode > SDL_SCANCODE_UNKNOWN && event->key.scancode < SDL_SCANCODE_COUNT) {
2635 bool* held = &menu->keysHeldAtOpen[event->key.scancode];
2636 if (event->type == SDL_EVENT_KEY_UP && *held) {
2637 *held = false;
2638 return false;
2639 }
2640 if (event->type == SDL_EVENT_KEY_DOWN && !event->key.repeat) {
2641 *held = false;
2642 }
2643 }
2644
2645 // Feed a copy so coordinate conversion doesn't mutate the caller's event.
2646 SDL_SetRenderScale(menu->lr->renderer, menu->renderScale, menu->renderScale);
2647 SDL_Event converted = *event;
2648 SDL_ConvertEventToRenderCoordinates(menu->lr->renderer, &converted);
2649 nk_sdl_handle_event(menu->ctx, &converted);
2650 SDL_SetRenderScale(menu->lr->renderer, 1.0f, 1.0f);
2651
2652 // Lifecycle events stay visible to the application and the frontend even
2653 // while the menu swallows gameplay input.
2654 switch (event->type) {
2655 case SDL_EVENT_QUIT:
2656 case SDL_EVENT_DROP_FILE:
2657 case SDL_EVENT_GAMEPAD_ADDED:
2658 case SDL_EVENT_GAMEPAD_REMOVED:
2659 case SDL_EVENT_JOYSTICK_ADDED:
2660 case SDL_EVENT_JOYSTICK_REMOVED:
2661 return false;
2662 default:
2663 break;
2664 }
2665 if (event->type >= SDL_EVENT_WINDOW_FIRST && event->type <= SDL_EVENT_WINDOW_LAST) {
2666 return false;
2667 }
2668 return true;
2669}
2670
2682static void SDL_Libretro_MenuRebuildCoreMenus(SDL_LibretroMenu* menu, bool justOpened) {
2683 SDL_Libretro* lr = menu->lr;
2685 menu->optionsStale = true;
2686 }
2687 // Any unload frees the strings the widgets borrow, even when the same core
2688 // loads again, so compare generations rather than core names.
2689 bool coreChanged = menu->builtCoreGeneration != lr->coreGeneration;
2690 bool countChanged = menu->builtOptionCount != lr->core.optionCount;
2691 if (!justOpened && !coreChanged && !countChanged && !menu->optionsStale) {
2692 return;
2693 }
2694
2695 if (justOpened && SDL_Libretro_UpdateOptionVisibility(lr)) {
2697 }
2698 SDL_Libretro_MenuBuildOptions(menu);
2699 SDL_Libretro_MenuBuildControllers(menu);
2700 SDL_Libretro_MenuBuildDisks(menu);
2701 // Not core-derived, but gamepads can come and go while the menu is
2702 // closed, and the player pages are titled with their names.
2703 SDL_Libretro_MenuBuildGamepads(menu);
2704 SDL_Libretro_MenuUpdateLoadGameFilter(menu);
2705 menu->optionsStale = false;
2706 menu->builtOptionCount = lr->core.optionCount;
2707 menu->builtCoreGeneration = lr->coreGeneration;
2708}
2709
2725bool SDL_Libretro_OpenMenuPath(SDL_LibretroMenu* menu, const char* path) {
2726 if (menu == NULL || menu->console == NULL || path == NULL) {
2727 return SDL_InvalidParamError("menu");
2728 }
2729 SDL_Libretro_SetMenuOpen(menu, true);
2730 // Do the just-opened work here so the next SDL_Libretro_UpdateMenu()
2731 // doesn't rebuild the lazy pages again underneath the navigation, and so
2732 // their paths resolve before the first frame.
2733 SDL_Libretro_MenuRebuildCoreMenus(menu, true);
2734 SDL_Libretro_MenuSyncSettings(menu);
2735 SDL_Libretro_MenuSyncSettingsBuffers(menu);
2736 SDL_Libretro_MenuSyncKeyBinds(menu);
2737 SDL_Libretro_MenuSyncPadBinds(menu);
2738 menu->wasOpen = true;
2739
2740 return nk_console_navigate_to_path(menu->console, path) == nk_true;
2741}
2742
2758void SDL_Libretro_UpdateMenu(SDL_LibretroMenu* menu) {
2759 if (menu == NULL || menu->ctx == NULL) {
2760 return;
2761 }
2762 SDL_Libretro* lr = menu->lr;
2763
2764 nk_input_end(menu->ctx);
2765 nk_gamepad_update(&menu->gamepads);
2766
2767 // Hide gamepad buttons held since the menu opened until they're let go,
2768 // so their release doesn't click whatever has focus.
2769 for (int i = 0; i < NK_GAMEPAD_MAX; i++) {
2770 struct nk_gamepad* pad = &menu->gamepads.gamepads[i];
2771 unsigned int held = menu->padButtonsHeldAtOpen[i];
2772 menu->padButtonsHeldAtOpen[i] = held & pad->buttons;
2773 pad->buttons &= ~held;
2774 pad->buttons_prev &= ~held;
2775 }
2776
2777 // Show the menu whenever there's nothing to run.
2778 if (!menu->open && !SDL_Libretro_IsGameReady(lr)) {
2779 SDL_Libretro_SetMenuOpen(menu, true);
2780 }
2781
2782 bool justOpened = menu->open && !menu->wasOpen;
2783 menu->wasOpen = menu->open;
2784 if (!menu->open) {
2785 return;
2786 }
2787
2788 SDL_Libretro_MenuRebuildCoreMenus(menu, justOpened);
2789
2790 // Game-dependent entries.
2791 bool gameReady = SDL_Libretro_IsGameReady(lr);
2792 menu->resumeButton->visible = (nk_bool)gameReady;
2793 menu->stateRow->visible = (nk_bool)gameReady;
2794 menu->screenshotButton->visible = (nk_bool)gameReady;
2795 menu->resetButton->visible = (nk_bool)gameReady;
2796
2797 // Progress-type OSD messages get a live bar at the top of the menu.
2798 int osdProgress = SDL_Libretro_GetMessageProgress(lr);
2799 if (osdProgress >= 0) {
2800 const char* osdMessage = SDL_Libretro_GetMessage(lr);
2801 SDL_strlcpy(menu->osdProgressLabel, osdMessage != NULL ? osdMessage : "", sizeof(menu->osdProgressLabel));
2802 menu->osdProgressValue = (nk_size)(osdProgress > 100 ? 100 : osdProgress);
2803 nk_console_progress_update(menu->osdProgressWidget, menu->osdProgressLabel, &menu->osdProgressValue, 100);
2804 menu->osdProgressWidget->visible = nk_true;
2805 }
2806 else {
2807 menu->osdProgressWidget->visible = nk_false;
2808 }
2809
2810 // Settings that can change outside the menu.
2811 if (justOpened) {
2812 SDL_Libretro_MenuSyncSettings(menu);
2813 SDL_Libretro_MenuSyncSettingsBuffers(menu);
2814 SDL_Libretro_MenuSyncKeyBinds(menu);
2815 SDL_Libretro_MenuSyncPadBinds(menu);
2816 }
2817
2818 // Flush changed menu settings into the config.
2819 if (menu->settingsDirty) {
2820 SDL_Libretro_MenuSaveState(menu);
2821 }
2822
2823 menu->renderScale = SDL_Libretro_MenuRenderScale(menu);
2824
2825 int width = 0;
2826 int height = 0;
2827 SDL_GetRenderOutputSize(lr->renderer, &width, &height);
2828 float logicalW = (float)width / menu->renderScale;
2829 float logicalH = (float)height / menu->renderScale;
2830
2831 menu->uiBuilt = true;
2832 nk_console_render_window(menu->console, "SDL_libretro", nk_rect(0.0f, 0.0f, logicalW, logicalH), NK_WINDOW_SCROLL_AUTO_HIDE |
2833 // Show the window title only on the top level.
2834 ((nk_console_active_parent(menu->console) == menu->console) ? NK_WINDOW_TITLE : 0));
2835}
2836
2848void SDL_Libretro_RenderMenu(SDL_LibretroMenu* menu) {
2849 if (menu == NULL || menu->ctx == NULL) {
2850 return;
2851 }
2852 // Flush whenever nk_begin ran this frame, even if a callback closed the
2853 // menu mid-frame, so nk_clear always pairs with it.
2854 if (menu->uiBuilt) {
2855 SDL_SetRenderScale(menu->lr->renderer, menu->renderScale, menu->renderScale);
2856 nk_sdl_render(menu->ctx, NK_ANTI_ALIASING_ON);
2857 SDL_SetRenderScale(menu->lr->renderer, 1.0f, 1.0f);
2858 nk_console_sdl_update_text_input(menu->console, menu->lr->window);
2859 menu->uiBuilt = false;
2860 }
2861 nk_input_begin(menu->ctx);
2862}
2863
2868static void SDL_Libretro_MenuKeepQuitLast(SDL_LibretroMenu* menu) {
2869 nk_console** children = menu->console->children;
2870 size_t count = cvector_size(children);
2871 if (menu->quitButton == NULL || count < 2 || children[count - 2] != menu->quitButton) {
2872 return;
2873 }
2874 children[count - 2] = children[count - 1];
2875 children[count - 1] = menu->quitButton;
2876}
2877
2894bool SDL_Libretro_AddMenuButton(SDL_LibretroMenu* menu, const char* label, SDL_LibretroMenuCallback callback, void* userdata) {
2895 if (menu == NULL || menu->console == NULL || label == NULL) {
2896 return SDL_InvalidParamError("menu");
2897 }
2898 nk_console* button = SDL_Libretro_MenuAddButton(menu, menu->console, label, NK_SYMBOL_NONE, callback, userdata);
2899 SDL_Libretro_MenuKeepQuitLast(menu);
2900 return button != NULL;
2901}
2902
2921bool SDL_Libretro_AddMenuCheckbox(SDL_LibretroMenu* menu, const char* label, bool* value, SDL_LibretroMenuCallback callback, void* userdata) {
2922 if (menu == NULL || menu->console == NULL || label == NULL) {
2923 return SDL_InvalidParamError("menu");
2924 }
2925 nk_console* checkbox = SDL_Libretro_MenuAddCheckbox(menu, menu->console, label, NULL, value, callback, userdata);
2926 SDL_Libretro_MenuKeepQuitLast(menu);
2927 return checkbox != NULL;
2928}
2929
2930#endif /* SDL_LIBRETRO_MENU_IMPL_ONCE */
#define SDL_LIBRETRO_MAX_USERS
The number of controller ports (gamepads) the frontend tracks.
#define SDL_LIBRETRO_MENU_FONT_HEIGHT
Base height in pixels for the menu font, multiplied by the UI scale and a whole-number display scale.
#define SDL_LIBRETRO_MENU_DEFAULT_STYLE
The style applied when the menu is created.
#define SDL_LIBRETRO_MENU_TOGGLE_KEY
The keyboard key that toggles the menu.
bool SDL_Libretro_IsMenuOpen(const SDL_LibretroMenu *menu)
Checks whether the menu is currently shown.
bool SDL_Libretro_IsCoreReady(const SDL_Libretro *lr)
Indicates whether a core is loaded.
bool SDL_Libretro_SetSystemDirectory(SDL_Libretro *lr, const char *path)
Sets where cores look for system files (BIOS, firmware, etc).
struct SDL_LibretroMenu SDL_LibretroMenu
An in-app menu built on nuklear_console.
size_t SDL_Libretro_GetSavePath(const SDL_Libretro *lr, const char *extension, char *dst, size_t dstSize)
Build a path in the save directory for the currently loaded content.
bool SDL_Libretro_SetCoreDirectory(SDL_Libretro *lr, const char *path)
Sets the associated libretro core directory, where the default set of cores will be loaded from.
void SDL_Libretro_SetRewindMemoryLimit(SDL_Libretro *lr, size_t maxBytes)
Set the maximum number of bytes of encoded delta history to retain.
bool SDL_Libretro_UpdateOptionVisibility(SDL_Libretro *lr)
Ask the core to refresh which of its options should be displayed.
void(* SDL_LibretroMenuCallback)(SDL_LibretroMenu *menu, void *userdata)
A callback for menu entries added by the application.
const char * SDL_Libretro_GetUsername(SDL_Libretro *lr)
Get the username reported to cores.
#define SDL_EVENT_LIBRETRO_MENU_OPENED
The menu became visible; the game pauses.
SDL_ScaleMode SDL_Libretro_GetScaleMode(const SDL_Libretro *lr)
Gets the texture scale mode used when the libretro frame is scaled.
SDL_LibretroFitMode SDL_Libretro_GetFitMode(const SDL_Libretro *lr)
Gets how the frame is fit into the destination rectangle when rendered.
bool SDL_Libretro_SaveScreenshot(const SDL_Libretro *lr, const char *path)
Saves a PNG screenshot of the current frame.
int SDL_Libretro_GetRotation(const SDL_Libretro *lr)
Gets the screen rotation requested by the core.
void SDL_Libretro_UpdateMenu(SDL_LibretroMenu *menu)
Processes input and lays out the menu for this frame.
#define SDL_LIBRETRO_MINOR_VERSION
The minor version of SDL_Libretro.
void SDL_Libretro_UnloadCore(SDL_Libretro *lr)
Unloads the actively loaded core.
int SDL_Libretro_GetMessageProgress(SDL_Libretro *lr)
Gets the progress of the message returned by SDL_Libretro_GetMessage().
bool SDL_Libretro_EjectDisk(SDL_Libretro *lr)
Ejects the disk tray, allowing the disk image to be changed.
SDL_LibretroFitMode
When rendering the libretro context, determine how to display within the destination.
bool SDL_Libretro_SetPortDevice(SDL_Libretro *lr, unsigned port, unsigned device)
Assign a device type to a controller port.
bool SDL_Libretro_Reset(SDL_Libretro *lr)
Resets the loaded game, like pressing the console's reset button.
size_t SDL_Libretro_GetFileName(char *dst, size_t dstSize, const char *path, bool withExtension)
Copy the file name portion of a path into a caller-provided buffer.
#define SDL_EVENT_LIBRETRO_MENU_CLOSED
The menu was dismissed; the game resumes.
bool SDL_Libretro_AreOptionsDirty(SDL_Libretro *lr)
Retrieves whether the options have been changed since the last time they were checked.
bool SDL_Libretro_PhysFS_LoadGame(SDL_Libretro *lr, const char *gamePath)
Archive-aware replacement for SDL_Libretro_LoadGame().
unsigned SDL_Libretro_GetPortDevice(const SDL_Libretro *lr, unsigned port)
Get the device type assigned to a controller port.
void * SDL_Libretro_GetMenuUserData(const SDL_LibretroMenu *menu)
Gets the data attached with SDL_Libretro_SetMenuUserData().
const SDL_LibretroCategory * SDL_Libretro_GetCategoryByIndex(const SDL_Libretro *lr, unsigned index)
Retrieve an option category by index.
SDL_LibretroMenuStyle
Color themes for the menu.
bool SDL_Libretro_SetRewindEnabled(SDL_Libretro *lr, bool enabled, unsigned bufferFrames, unsigned captureInterval)
Enable or disable the rewind system.
const char * SDL_Libretro_GetCoreVersion(const SDL_Libretro *lr)
Retrieves the version of the libretro core that's actively loaded.
void SDL_Libretro_RenderMenu(SDL_LibretroMenu *menu)
Draws the menu laid out by SDL_Libretro_UpdateMenu() to the renderer.
void SDL_Libretro_DestroyMenu(SDL_LibretroMenu *menu)
Destroys a menu created with SDL_Libretro_CreateMenu().
const char * SDL_Libretro_GetValidExtensions(const SDL_Libretro *lr)
Gets the default set of valid extensions associated with the core, seperated by a "|".
void SDL_Libretro_SetMenuOpen(SDL_LibretroMenu *menu, bool open)
Opens or closes the menu.
unsigned SDL_Libretro_GetCategoryCount(const SDL_Libretro *lr)
Get the number of option categories registered by the core.
bool SDL_Libretro_AddMenuCheckbox(SDL_LibretroMenu *menu, const char *label, bool *value, SDL_LibretroMenuCallback callback, void *userdata)
Adds a checkbox to the main menu, just above Quit.
unsigned SDL_Libretro_GetDiskIndex(const SDL_Libretro *lr)
Returns the index of the currently inserted disk image.
bool SDL_Libretro_SetDiskIndex(SDL_Libretro *lr, unsigned index)
Sets the disk image index to insert into the emulated drive.
const char * SDL_Libretro_GetMessage(SDL_Libretro *lr)
Gets the most relevant libretro message to display from the queue.
double SDL_Libretro_GetFPS(const SDL_Libretro *lr)
Gets the frame rate the core expects to run at.
bool SDL_Libretro_LoadState(SDL_Libretro *lr, const char *file)
Loads the libretro state from the given file.
bool SDL_Libretro_SetFitMode(SDL_Libretro *lr, SDL_LibretroFitMode mode)
Sets how the frame is fit into the destination rectangle when rendered.
const SDL_LibretroOption * SDL_Libretro_GetOptionByIndex(const SDL_Libretro *lr, unsigned index)
Retrieve a core option by its registration index.
void SDL_Libretro_SetVolume(SDL_Libretro *lr, float volume)
Set the audio volume when playing sounds.
struct SDL_Libretro SDL_Libretro
A libretro frontend context: one loaded core, its game, and the settings that carry across cores.
bool SDL_Libretro_GetDiskLabel(const SDL_Libretro *lr, unsigned index, char *label, size_t len)
Retrieves the human-readable label of a disk image.
bool SDL_Libretro_LoadGame(SDL_Libretro *lr, const char *gamePath)
Loads a game at the given path.
float SDL_Libretro_GetAspectRatio(const SDL_Libretro *lr)
Gets the aspect ratio for the libretro context.
SDL_Libretro * SDL_Libretro_GetMenuLibretro(const SDL_LibretroMenu *menu)
Gets the libretro context the menu was created for.
void SDL_Libretro_SetGamepadMapping(SDL_Libretro *lr, unsigned port, int retroButton, SDL_GamepadButton button)
Maps an SDL gamepad button to a RETRO_DEVICE_ID_JOYPAD_* button on the given controller port.
float SDL_Libretro_GetVolume(const SDL_Libretro *lr)
Get the audio volume.
bool SDL_Libretro_OpenMenuPath(SDL_LibretroMenu *menu, const char *path)
Opens the menu and navigates to a page by its slash-separated label path, e.g.
void SDL_Libretro_SetMousePort(SDL_Libretro *lr, unsigned port)
Selects the one controller port that the mouse drives.
bool SDL_Libretro_SetMenuStyle(SDL_LibretroMenu *menu, SDL_LibretroMenuStyle style)
Applies a color theme to the menu.
SDL_LibretroMenu * SDL_Libretro_CreateMenu(SDL_Libretro *lr)
Creates the in-app menu for a libretro context.
bool SDL_Libretro_GetRewindEnabled(const SDL_Libretro *lr)
Check whether the rewind system is enabled (independent of current direction).
#define SDL_LIBRETRO_MAJOR_VERSION
The major version of SDL_Libretro.
const char * SDL_Libretro_GetSystemDirectory(SDL_Libretro *lr)
Get the system directory.
bool SDL_Libretro_SetOptionValue(SDL_Libretro *lr, const char *key, const char *value)
Set a core option's value; fails if the value isn't one the core declared.
void SDL_Libretro_ResetAllOptions(SDL_Libretro *lr)
Reset every core option to its default value.
bool SDL_Libretro_SetSaveDirectory(SDL_Libretro *lr, const char *path)
Sets where content will be saved.
unsigned SDL_Libretro_GetDiskCount(const SDL_Libretro *lr)
Returns the number of disk images available to the loaded core.
bool SDL_Libretro_SaveState(SDL_Libretro *lr, const char *file)
Saves the current libretro state to a file.
size_t SDL_Libretro_GetRewindMemoryLimit(const SDL_Libretro *lr)
Get the current rewind memory budget in bytes (0 if unbounded).
void SDL_Libretro_GetSize(const SDL_Libretro *lr, int *w, int *h)
Gets the width and height of the core's current video frame.
unsigned SDL_Libretro_GetOptionCount(const SDL_Libretro *lr)
Get the number of core options that have been set.
void SDL_Libretro_SetMenuUserData(SDL_LibretroMenu *menu, void *userData)
Attaches generic application data to the menu.
bool SDL_Libretro_IsGameReady(const SDL_Libretro *lr)
Indicates whether a game is loaded and running.
bool SDL_Libretro_InsertDisk(SDL_Libretro *lr)
Closes the disk tray after a disk change.
bool SDL_Libretro_SetUsername(SDL_Libretro *lr, const char *username)
Sets the username reported to cores through RETRO_ENVIRONMENT_GET_USERNAME.
bool SDL_Libretro_AddMenuButton(SDL_LibretroMenu *menu, const char *label, SDL_LibretroMenuCallback callback, void *userdata)
Adds a button to the main menu, just above Quit.
SDL_LibretroMenuStyle SDL_Libretro_GetMenuStyle(const SDL_LibretroMenu *menu)
Gets the menu's active color theme.
void SDL_Libretro_MenuScreenshotClicked(SDL_LibretroMenu *menu, void *userdata)
A ready-made menu callback that saves a PNG screenshot of the current frame, reporting the result in ...
bool SDL_Libretro_SetScaleMode(SDL_Libretro *lr, SDL_ScaleMode mode)
Sets the texture scale mode used when the libretro frame is scaled.
void SDL_Libretro_SetAnalogToDigital(SDL_Libretro *lr, unsigned port, SDL_LibretroAnalogToDigital stick)
Selects which analog stick, if any, also registers D-Pad presses on the given controller port,...
const char * SDL_Libretro_GetCoreDirectory(SDL_Libretro *lr)
Get the core directory.
const char * SDL_Libretro_GetSaveDirectory(SDL_Libretro *lr)
Get the save directory.
bool SDL_Libretro_HandleMenuEvent(SDL_LibretroMenu *menu, const SDL_Event *event)
Routes an event to the menu; call this for every event, before SDL_Libretro_HandleEvent().
void SDL_Libretro_ToggleMenu(SDL_LibretroMenu *menu)
Opens the menu when it is closed, or closes it when it is open.
SDL_LibretroAnalogToDigital
Which analog stick, if any, also registers D-Pad presses for a controller port.
unsigned SDL_Libretro_GetMousePort(const SDL_Libretro *lr)
Get the controller port that the mouse drives.
void SDL_Libretro_SetLanguage(SDL_Libretro *lr, int language)
Override the language reported to cores through RETRO_ENVIRONMENT_GET_LANGUAGE.
#define SDL_LIBRETRO_MICRO_VERSION
The micro/patch version of SDL_Libretro.
const char * SDL_Libretro_GetCoreName(const SDL_Libretro *lr)
Retrieve the name of the libretro core that's actively loaded.
void SDL_Libretro_SetKeyboardMapping(SDL_Libretro *lr, int retroButton, SDL_Scancode scancode)
Maps a keyboard key to a RETRO_DEVICE_ID_JOYPAD_* button for Player 1 (controller port 0).
@ SDL_LIBRETRO_MENU_STYLE_COUNT
The number of menu styles.
A group of related core options.
const char * key
Unique identifier referenced by SDL_LibretroOption::category.
const char * desc
Human-readable name.
const char * info
Description / help text; may be empty.
A core option and its current state.
bool visible
Whether the frontend should display it.
SDL_LibretroOptionValue * values
The selectable values; dynamically allocated.
const char * desc
Human-readable name.
const char * value
The current value.
const char * category
Key of the category it belongs to; empty if none.
unsigned valuesCount
The number of populated entries in values.
const char * key
Unique identifier the core queries.
const char * info
Description / help text; may be empty.
const char * value
The value the core stores.
const char * label
Human-readable name; may be NULL, in which case display value itself.