diff --git a/README.md b/README.md
index a293baa346..0789107d27 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,42 @@
+A fork of NextUI to improve usability for myself and make the OS easier to use for my parents.
+
+**This repo is a hobby project, it is not actively maintained.**
+
+If you want to use NextUI for your own handheld, get it from [Love Retro](https://github.com/LoveRetro/NextUI).
+
+## AI Disclaimer
+
+In the interest of full transparency: this fork was developed using AI tooling, specifically Claude Code.
+
+I've been a professional software developer since 2020 and AI agents are just part of my workflow. I have never developed C or C++ professionally (though I do like to use C for hobby projects) so while I've reviewed every code change, I am limited by my lack of experience, esp for C-specific things like safe memory management. (If you care, my primary languages are C# for backend/desktop and TypeScript for frontend.)
+
+I have done my best to keep PRs and commits small for reviewability should anyone want to cherry pick features out of this fork.
+
+## Context
+
+I got my parents an ANBERNIC RG 40XXH for Christmas so they could play their childhood games, but I knew immediately I was not going to be using the stock OS. After trying a few options I landed on using NextUI because it was focused and booted in and out of games extremely fast. It still felt open and breakable though, so I started this personal fork to lock it down and remove some footguns that I shot myself with in the past using NextUI on my own ANBERNIC RG SP.
+
+## Changes
+
+Everything was developed as atomic features that could be toggled via the settings. I did my best to develop the features in one PR per feature-setting to make cherry-picking easier if other folks want to take some of these. Every PR is tagged with what feature it touched for easier navigation.
+
+In order of smallest to largest:
+
+| Feature | Setting | Description | Reason | Limitations |
+|-|-|-|-|-|
+| Screen margin | Appearance / Screen margin | Adjusts the margin between the edge of the screen and widgets like the battery and input prompts. | Reclaims space on low resolution devices | Overrides both low res and high res / external displays with the same value when by default they are different. |
+| Quick menu title | Appearance / Show Quick Menu title | Shows a title for each quick setting button in the top left of the screen. | Remove ambiguity in what the icon-only buttons do in the quick menu. | |
+| Quick menu tiles | Appearch / Show X in Quick Menu | A series of options to disable the tiles that appear in the quick menu, specifically recents, games, and tools. | Recents and games as options don't make sense for my needs and at that point it was just tools so I cut it too for a cleaner look. | |
+| All games list | Appearance / All games view | Skips the system menu and just shows all games in a single alphabatized list. | With a small games library (10-12 games) the system menu was more a burden than anything. | Recently played doesn't have anywhere to live so only exists in the quick menu. |
+| Resume with A | System / Resume with A | Swaps the A/X behavior so resume is mapped to A and open is mapped to X | A is the default way someone would try to launch a game so this effectively lets someone make resuming their default action. | |
+| Save & Quit | System / Allow Save & Quit from menu | Adds the Save & Quit option to the in-game menu if the game is targeting a save slot already. | Remembering a hotkey is more annoying than using the menu. | |
+| Grid view | Appearance / Content layout + Appearance / Grid rows | Show the list in a horizontal grid of icon buttons, rendering the game title in the top left corner. | A horizontal grid is more familiar to people trained on smartphones. | Entries that don't have an icon defined use their first letter as their icon and those icons have to be provided manually. |
+
+## Testing
+
+All testing has been done on device with the H700 powered ANBERNIC RG 40XXH. I don't have any devices running other NextUI supported architectures to test on them.
+
+

diff --git a/workspace/all/common/api.c b/workspace/all/common/api.c
index e107225da5..ac29152421 100644
--- a/workspace/all/common/api.c
+++ b/workspace/all/common/api.c
@@ -362,12 +362,25 @@ FALLBACK_IMPLEMENTATION void PLAT_getGPUUsage()
perf.gpu_usage = 0.0;
}
+static char system_font_path[MAX_PATH] = "";
+
+const char *GFX_getSystemFontPath(void)
+{
+ return system_font_path;
+}
+
int GFX_loadSystemFont(const char *fontPath)
{
// Load/Reload fonts
if (!TTF_WasInit())
TTF_Init();
+ if (fontPath && fontPath != system_font_path)
+ {
+ strncpy(system_font_path, fontPath, sizeof(system_font_path) - 1);
+ system_font_path[sizeof(system_font_path) - 1] = '\0';
+ }
+
TTF_CloseFont(font.large);
TTF_CloseFont(font.medium);
TTF_CloseFont(font.small);
@@ -2356,18 +2369,25 @@ SDL_Surface* GFX_createScreenFormatSurface(int width, int height)
);
}
+int GFX_getScreenMargin(void)
+{
+ int margin = CFG_getScreenMargin();
+ return SCALE1(margin < 0 ? PADDING : margin);
+}
+
int GFX_blitHardwareGroup(SDL_Surface *dst, int show_setting)
{
int ox;
int oy;
int ow = 0;
+ int margin = GFX_getScreenMargin();
if (show_setting && !GetHDMI())
{
// Use the helper function to render the indicator at the standard position
ow = SCALE1(PILL_SIZE + SETTINGS_WIDTH + 10 + 4);
- ox = dst->w - SCALE1(PADDING) - ow;
- oy = SCALE1(PADDING);
+ ox = dst->w - margin - ow;
+ oy = margin;
GFX_blitHardwareIndicator(dst, ox, oy, (IndicatorType)show_setting);
}
else
@@ -2384,8 +2404,8 @@ int GFX_blitHardwareGroup(SDL_Surface *dst, int show_setting)
if (!show_bt && !show_wifi && !show_clock)
{
ow = SCALE1(PILL_SIZE);
- ox = dst->w - SCALE1(PADDING) - ow;
- oy = SCALE1(PADDING);
+ ox = dst->w - margin - ow;
+ oy = margin;
GFX_blitPillColor(ASSET_WHITE_PILL, dst, &(SDL_Rect){ox, oy, ow, SCALE1(PILL_SIZE)}, THEME_COLOR2, RGB_WHITE);
@@ -2435,8 +2455,8 @@ int GFX_blitHardwareGroup(SDL_Surface *dst, int show_setting)
ow += clock_width + SCALE1(BUTTON_MARGIN);
}
- ox = dst->w - SCALE1(PADDING) - ow;
- oy = SCALE1(PADDING);
+ ox = dst->w - margin - ow;
+ oy = margin;
GFX_blitPillColor(ASSET_WHITE_PILL, dst, &(SDL_Rect){ox, oy, ow, SCALE1(PILL_SIZE)}, THEME_COLOR2, RGB_WHITE);
ox += SCALE1(BUTTON_MARGIN);
@@ -2523,8 +2543,9 @@ int GFX_blitButtonGroup(char **pairs, int primary, SDL_Surface *dst, int align_r
int w = 0; // individual button dimension
int h = 0; // hints index
ow = 0; // full pill width
- ox = align_right ? dst->w - SCALE1(PADDING) : SCALE1(PADDING);
- oy = dst->h - SCALE1(PADDING + PILL_SIZE);
+ int margin = GFX_getScreenMargin();
+ ox = align_right ? dst->w - margin : margin;
+ oy = dst->h - margin - SCALE1(PILL_SIZE);
for (int i = 0; i < 2; i++)
{
diff --git a/workspace/all/common/api.h b/workspace/all/common/api.h
index e7b5d6808c..e8a1b71137 100644
--- a/workspace/all/common/api.h
+++ b/workspace/all/common/api.h
@@ -401,7 +401,10 @@ int GFX_getButtonWidth(char* hint, char* button);
void GFX_blitButton(char* hint, char*button, SDL_Surface* dst, SDL_Rect* dst_rect);
void GFX_blitMessage(TTF_Font* font, char* msg, SDL_Surface* dst, SDL_Rect* dst_rect);
-int GFX_blitHardwareGroup(SDL_Surface* dst, int show_setting);
+int GFX_blitHardwareGroup(SDL_Surface* dst, int show_setting); // returns the status pill's width
+// Scaled distance between the screen edges and edge-anchored UI (status pill,
+// button hints, list title): the "Screen margin" setting, PADDING by default.
+int GFX_getScreenMargin(void);
void GFX_blitHardwareHints(SDL_Surface* dst, int show_setting);
void GFX_blitTopCurtain(SDL_Surface* dst);
void GFX_blitBottomCurtain(SDL_Surface* dst);
@@ -449,6 +452,8 @@ void GFX_ApplyRoundedCorners16(SDL_Surface* surface, SDL_Rect* rect, int radius)
void GFX_ApplyRoundedCorners_4444(SDL_Surface* surface, SDL_Rect* rect, int radius);
// for both ARGB8888 and RGBA8888
void GFX_ApplyRoundedCorners_8888(SDL_Surface* surface, SDL_Rect* rect, int radius);
+// Path of the currently loaded UI font file ("" before GFX_init), for opening it at other sizes
+const char* GFX_getSystemFontPath(void);
///////////////////////////////
typedef struct SND_Frame {
diff --git a/workspace/all/common/config.c b/workspace/all/common/config.c
index aea4cb4967..68fbc1c727 100644
--- a/workspace/all/common/config.c
+++ b/workspace/all/common/config.c
@@ -60,7 +60,16 @@ void CFG_defaults(NextUISettings *cfg)
.showGameArt = CFG_DEFAULT_SHOWGAMEART,
.gameSwitcherScaling = CFG_DEFAULT_GAMESWITCHERSCALING,
.defaultView = CFG_DEFAULT_VIEW,
+ .contentLayout = CFG_DEFAULT_CONTENTLAYOUT,
+ .gridRows = CFG_DEFAULT_GRIDROWS,
+ .screenMargin = CFG_DEFAULT_SCREENMARGIN,
+ .resumeWithA = CFG_DEFAULT_RESUMEWITHA,
+ .allGamesView = CFG_DEFAULT_ALLGAMESVIEW,
.showQuickSwitcherUi = CFG_DEFAULT_SHOWQUICKWITCHERUI,
+ .showQuickMenuTitle = CFG_DEFAULT_SHOWQUICKMENUTITLE,
+ .quickMenuRecents = CFG_DEFAULT_QUICKMENURECENTS,
+ .quickMenuGames = CFG_DEFAULT_QUICKMENUGAMES,
+ .quickMenuTools = CFG_DEFAULT_QUICKMENUTOOLS,
.gameSwitcherCurtain = CFG_DEFAULT_GAMESWITCHER_CURTAIN,
.muteLeds = CFG_DEFAULT_MUTELEDS,
@@ -76,6 +85,7 @@ void CFG_defaults(NextUISettings *cfg)
.romsUseFolderBackground = CFG_DEFAULT_ROMSUSEFOLDERBACKGROUND,
.saveFormat = CFG_DEFAULT_SAVEFORMAT,
.stateFormat = CFG_DEFAULT_STATEFORMAT,
+ .saveQuitMenu = CFG_DEFAULT_SAVEQUITMENU,
.useExtractedFileName = CFG_DEFAULT_EXTRACTEDFILENAME,
.ntp = CFG_DEFAULT_NTP,
@@ -336,6 +346,11 @@ void CFG_init(FontLoad_callback_t cb, ColorSet_callback_t ccb)
CFG_setStateFormat(temp_value);
continue;
}
+ if (sscanf(line, "saveQuitMenu=%i", &temp_value) == 1)
+ {
+ CFG_setSaveQuitMenu((bool)temp_value);
+ continue;
+ }
if (sscanf(line, "useExtractedFileName=%i", &temp_value) == 1)
{
CFG_setUseExtractedFileName((bool)temp_value);
@@ -382,11 +397,56 @@ void CFG_init(FontLoad_callback_t cb, ColorSet_callback_t ccb)
CFG_setDefaultView(temp_value);
continue;
}
+ if (sscanf(line, "contentLayout=%i", &temp_value) == 1)
+ {
+ CFG_setContentLayout(temp_value);
+ continue;
+ }
+ if (sscanf(line, "gridRows=%i", &temp_value) == 1)
+ {
+ CFG_setGridRows(temp_value);
+ continue;
+ }
+ if (sscanf(line, "resumeWithA=%i", &temp_value) == 1)
+ {
+ CFG_setResumeWithA((bool)temp_value);
+ continue;
+ }
+ if (sscanf(line, "screenMargin=%i", &temp_value) == 1)
+ {
+ CFG_setScreenMargin(temp_value);
+ continue;
+ }
+ if (sscanf(line, "allGamesView=%i", &temp_value) == 1)
+ {
+ CFG_setAllGamesView((bool)temp_value);
+ continue;
+ }
if (sscanf(line, "quickSwitcherUi=%i", &temp_value) == 1)
{
CFG_setShowQuickswitcherUI(temp_value);
continue;
}
+ if (sscanf(line, "quickMenuTitle=%i", &temp_value) == 1)
+ {
+ CFG_setShowQuickMenuTitle((bool)temp_value);
+ continue;
+ }
+ if (sscanf(line, "quickMenuRecents=%i", &temp_value) == 1)
+ {
+ CFG_setQuickMenuRecents((bool)temp_value);
+ continue;
+ }
+ if (sscanf(line, "quickMenuGames=%i", &temp_value) == 1)
+ {
+ CFG_setQuickMenuGames((bool)temp_value);
+ continue;
+ }
+ if (sscanf(line, "quickMenuTools=%i", &temp_value) == 1)
+ {
+ CFG_setQuickMenuTools((bool)temp_value);
+ continue;
+ }
if (sscanf(line, "wifiDiagnostics=%i", &temp_value) == 1)
{
CFG_setWifiDiagnostics(temp_value);
@@ -914,6 +974,17 @@ void CFG_setStateFormat(int f)
CFG_sync();
}
+bool CFG_getSaveQuitMenu(void)
+{
+ return settings.saveQuitMenu;
+}
+
+void CFG_setSaveQuitMenu(bool on)
+{
+ settings.saveQuitMenu = on;
+ CFG_sync();
+}
+
bool CFG_getUseExtractedFileName(void)
{
return settings.useExtractedFileName;
@@ -987,6 +1058,61 @@ void CFG_setDefaultView(int view)
CFG_sync();
}
+int CFG_getContentLayout(void)
+{
+ return settings.contentLayout;
+}
+
+void CFG_setContentLayout(int layout)
+{
+ settings.contentLayout = (layout == CONTENT_LAYOUT_GRID) ? CONTENT_LAYOUT_GRID : CONTENT_LAYOUT_LIST;
+ CFG_sync();
+}
+
+int CFG_getGridRows(void)
+{
+ return settings.gridRows;
+}
+
+void CFG_setGridRows(int rows)
+{
+ settings.gridRows = clamp(rows, GRID_ROWS_MIN, GRID_ROWS_MAX);
+ CFG_sync();
+}
+
+int CFG_getScreenMargin(void)
+{
+ return settings.screenMargin;
+}
+
+void CFG_setScreenMargin(int margin)
+{
+ settings.screenMargin = (margin < 0) ? SCREEN_MARGIN_DEFAULT : clamp(margin, 0, SCREEN_MARGIN_MAX);
+ CFG_sync();
+}
+
+bool CFG_getResumeWithA(void)
+{
+ return settings.resumeWithA;
+}
+
+void CFG_setResumeWithA(bool on)
+{
+ settings.resumeWithA = on;
+ CFG_sync();
+}
+
+bool CFG_getAllGamesView(void)
+{
+ return settings.allGamesView;
+}
+
+void CFG_setAllGamesView(bool on)
+{
+ settings.allGamesView = on;
+ CFG_sync();
+}
+
bool CFG_getShowQuickswitcherUI(void)
{
return settings.showQuickSwitcherUi;
@@ -998,6 +1124,50 @@ void CFG_setShowQuickswitcherUI(bool on)
CFG_sync();
}
+bool CFG_getShowQuickMenuTitle(void)
+{
+ return settings.showQuickMenuTitle;
+}
+
+void CFG_setShowQuickMenuTitle(bool on)
+{
+ settings.showQuickMenuTitle = on;
+ CFG_sync();
+}
+
+bool CFG_getQuickMenuRecents(void)
+{
+ return settings.quickMenuRecents;
+}
+
+void CFG_setQuickMenuRecents(bool on)
+{
+ settings.quickMenuRecents = on;
+ CFG_sync();
+}
+
+bool CFG_getQuickMenuGames(void)
+{
+ return settings.quickMenuGames;
+}
+
+void CFG_setQuickMenuGames(bool on)
+{
+ settings.quickMenuGames = on;
+ CFG_sync();
+}
+
+bool CFG_getQuickMenuTools(void)
+{
+ return settings.quickMenuTools;
+}
+
+void CFG_setQuickMenuTools(bool on)
+{
+ settings.quickMenuTools = on;
+ CFG_sync();
+}
+
bool CFG_getWifiDiagnostics(void)
{
return settings.wifiDiagnostics;
@@ -1450,6 +1620,10 @@ void CFG_get(const char *key, char *value)
{
sprintf(value, "%i", CFG_getStateFormat());
}
+ else if (strcmp(key, "saveQuitMenu") == 0)
+ {
+ sprintf(value, "%i", (int)(CFG_getSaveQuitMenu()));
+ }
else if (strcmp(key, "useExtractedFileName") == 0)
{
sprintf(value, "%i", CFG_getUseExtractedFileName());
@@ -1482,10 +1656,46 @@ void CFG_get(const char *key, char *value)
{
sprintf(value, "%i", (int)(CFG_getDefaultView()));
}
+ else if (strcmp(key, "contentLayout") == 0)
+ {
+ sprintf(value, "%i", CFG_getContentLayout());
+ }
+ else if (strcmp(key, "gridRows") == 0)
+ {
+ sprintf(value, "%i", CFG_getGridRows());
+ }
+ else if (strcmp(key, "screenMargin") == 0)
+ {
+ sprintf(value, "%i", CFG_getScreenMargin());
+ }
+ else if (strcmp(key, "resumeWithA") == 0)
+ {
+ sprintf(value, "%i", (int)(CFG_getResumeWithA()));
+ }
+ else if (strcmp(key, "allGamesView") == 0)
+ {
+ sprintf(value, "%i", (int)(CFG_getAllGamesView()));
+ }
else if (strcmp(key, "quickSwitcherUi") == 0)
{
sprintf(value, "%i", (int)(CFG_getShowQuickswitcherUI()));
}
+ else if (strcmp(key, "quickMenuTitle") == 0)
+ {
+ sprintf(value, "%i", (int)(CFG_getShowQuickMenuTitle()));
+ }
+ else if (strcmp(key, "quickMenuRecents") == 0)
+ {
+ sprintf(value, "%i", (int)(CFG_getQuickMenuRecents()));
+ }
+ else if (strcmp(key, "quickMenuGames") == 0)
+ {
+ sprintf(value, "%i", (int)(CFG_getQuickMenuGames()));
+ }
+ else if (strcmp(key, "quickMenuTools") == 0)
+ {
+ sprintf(value, "%i", (int)(CFG_getQuickMenuTools()));
+ }
else if (strcmp(key, "wifiDiagnostics") == 0)
{
sprintf(value, "%i", (int)(CFG_getWifiDiagnostics()));
@@ -1653,6 +1863,7 @@ void CFG_sync(void)
fprintf(file, "romfolderbg=%i\n", settings.romsUseFolderBackground);
fprintf(file, "saveFormat=%i\n", settings.saveFormat);
fprintf(file, "stateFormat=%i\n", settings.stateFormat);
+ fprintf(file, "saveQuitMenu=%i\n", settings.saveQuitMenu);
fprintf(file, "useExtractedFileName=%i\n", settings.useExtractedFileName);
fprintf(file, "muteLeds=%i\n", settings.muteLeds);
fprintf(file, "fn1action=%s\n", settings.fnAction[0]);
@@ -1661,7 +1872,16 @@ void CFG_sync(void)
fprintf(file, "artWidth=%i\n", (int)(settings.gameArtWidth * 100));
fprintf(file, "wifi=%i\n", settings.wifi);
fprintf(file, "defaultView=%i\n", settings.defaultView);
+ fprintf(file, "contentLayout=%i\n", settings.contentLayout);
+ fprintf(file, "gridRows=%i\n", settings.gridRows);
+ fprintf(file, "screenMargin=%i\n", settings.screenMargin);
+ fprintf(file, "resumeWithA=%i\n", settings.resumeWithA);
+ fprintf(file, "allGamesView=%i\n", settings.allGamesView);
fprintf(file, "quickSwitcherUi=%i\n", settings.showQuickSwitcherUi);
+ fprintf(file, "quickMenuTitle=%i\n", settings.showQuickMenuTitle);
+ fprintf(file, "quickMenuRecents=%i\n", settings.quickMenuRecents);
+ fprintf(file, "quickMenuGames=%i\n", settings.quickMenuGames);
+ fprintf(file, "quickMenuTools=%i\n", settings.quickMenuTools);
fprintf(file, "wifiDiagnostics=%i\n", settings.wifiDiagnostics);
fprintf(file, "bluetooth=%i\n", settings.bluetooth);
fprintf(file, "btDiagnostics=%i\n", settings.bluetoothDiagnostics);
@@ -1728,6 +1948,7 @@ void CFG_print(void)
printf("\t\"romfolderbg\": %i,\n", settings.romsUseFolderBackground);
printf("\t\"saveFormat\": %i,\n", settings.saveFormat);
printf("\t\"stateFormat\": %i,\n", settings.stateFormat);
+ printf("\t\"saveQuitMenu\": %i,\n", settings.saveQuitMenu);
printf("\t\"useExtractedFileName\": %i,\n", settings.useExtractedFileName);
printf("\t\"muteLeds\": %i,\n", settings.muteLeds);
printf("\t\"fn1action\": \"%s\",\n", settings.fnAction[0]);
@@ -1736,7 +1957,16 @@ void CFG_print(void)
printf("\t\"artWidth\": %i,\n", (int)(settings.gameArtWidth * 100));
printf("\t\"wifi\": %i,\n", settings.wifi);
printf("\t\"defaultView\": %i,\n", settings.defaultView);
+ printf("\t\"contentLayout\": %i,\n", settings.contentLayout);
+ printf("\t\"gridRows\": %i,\n", settings.gridRows);
+ printf("\t\"screenMargin\": %i,\n", settings.screenMargin);
+ printf("\t\"resumeWithA\": %i,\n", settings.resumeWithA);
+ printf("\t\"allGamesView\": %i,\n", settings.allGamesView);
printf("\t\"quickSwitcherUi\": %i,\n", settings.showQuickSwitcherUi);
+ printf("\t\"quickMenuTitle\": %i,\n", settings.showQuickMenuTitle);
+ printf("\t\"quickMenuRecents\": %i,\n", settings.quickMenuRecents);
+ printf("\t\"quickMenuGames\": %i,\n", settings.quickMenuGames);
+ printf("\t\"quickMenuTools\": %i,\n", settings.quickMenuTools);
printf("\t\"wifiDiagnostics\": %i,\n", settings.wifiDiagnostics);
printf("\t\"bluetooth\": %i,\n", settings.bluetooth);
printf("\t\"btDiagnostics\": %i,\n", settings.bluetoothDiagnostics);
diff --git a/workspace/all/common/config.h b/workspace/all/common/config.h
index bad3f5fc00..6a7650980d 100644
--- a/workspace/all/common/config.h
+++ b/workspace/all/common/config.h
@@ -55,6 +55,14 @@ enum {
SCREEN_OFF
};
+// How the game list and system list are laid out
+enum {
+ CONTENT_LAYOUT_LIST,
+ CONTENT_LAYOUT_GRID,
+};
+#define GRID_ROWS_MIN 1
+#define GRID_ROWS_MAX 4
+
// Achievement sort order options
enum {
RA_SORT_UNLOCKED_FIRST,
@@ -139,7 +147,16 @@ typedef struct
bool showFolderNamesAtRoot;
bool romsUseFolderBackground;
bool showQuickSwitcherUi;
+ bool showQuickMenuTitle; // name the selected Quick Menu item in the top-left pill
+ bool quickMenuRecents; // Quick Menu top row shows Recents (when there are any)
+ bool quickMenuGames; // Quick Menu top row shows Games
+ bool quickMenuTools; // Quick Menu top row shows Tools (when there are any)
int defaultView;
+ int contentLayout; // CONTENT_LAYOUT_*
+ int gridRows; // GRID_ROWS_MIN..GRID_ROWS_MAX
+ int screenMargin; // unscaled px between screen edges and edge UI, SCREEN_MARGIN_DEFAULT = PADDING
+ bool allGamesView; // root shows every game (A-Z) instead of the system list
+ bool resumeWithA; // A resumes a game's last save state, X starts it fresh
// Mute switch
bool muteLeds;
@@ -157,6 +174,7 @@ typedef struct
// Emulator
int saveFormat;
int stateFormat;
+ bool saveQuitMenu; // in-game menu offers Save & Quit into the session's slot
bool useExtractedFileName;
// Haptic
@@ -235,13 +253,27 @@ typedef struct
#define CFG_DEFAULT_ROMSUSEFOLDERBACKGROUND true
#define CFG_DEFAULT_SAVEFORMAT SAVE_FORMAT_SAV
#define CFG_DEFAULT_STATEFORMAT STATE_FORMAT_SAV
+#define CFG_DEFAULT_SAVEQUITMENU false
#define CFG_DEFAULT_EXTRACTEDFILENAME false
#define CFG_DEFAULT_MUTELEDS false
#define CFG_DEFAULT_FN_ACTION "" // unassigned
#define CFG_DEFAULT_GAMEARTWIDTH 0.45
#define CFG_DEFAULT_WIFI false
#define CFG_DEFAULT_VIEW SCREEN_GAMELIST
+#define CFG_DEFAULT_CONTENTLAYOUT CONTENT_LAYOUT_LIST
+#define CFG_DEFAULT_GRIDROWS 2
+// Screen margin: PADDING until changed (or reset). PADDING can depend on the
+// active display at runtime (eg. h700 HDMI/720p), so the default is a sentinel.
+#define SCREEN_MARGIN_DEFAULT -1
+#define SCREEN_MARGIN_MAX 20
+#define CFG_DEFAULT_SCREENMARGIN SCREEN_MARGIN_DEFAULT
+#define CFG_DEFAULT_ALLGAMESVIEW false
+#define CFG_DEFAULT_RESUMEWITHA false
#define CFG_DEFAULT_SHOWQUICKWITCHERUI true
+#define CFG_DEFAULT_SHOWQUICKMENUTITLE true
+#define CFG_DEFAULT_QUICKMENURECENTS true
+#define CFG_DEFAULT_QUICKMENUGAMES true
+#define CFG_DEFAULT_QUICKMENUTOOLS true
#define CFG_DEFAULT_WIFI_DIAG false
#define CFG_DEFAULT_SHOWTOOLS true
#define CFG_DEFAULT_BLUETOOTH false
@@ -380,6 +412,10 @@ void CFG_setSaveFormat(int);
// 1 - .state.
(compressed rzip)
int CFG_getStateFormat(void);
void CFG_setStateFormat(int);
+// Show Save & Quit (above Quit) in the in-game menu once the session has a save
+// slot: the slot it resumed from, or one saved/loaded during play.
+bool CFG_getSaveQuitMenu(void);
+void CFG_setSaveQuitMenu(bool on);
// use extracted file name instead of archive name (for cores that do not support archives natively)
bool CFG_getUseExtractedFileName(void);
void CFG_setUseExtractedFileName(bool);
@@ -407,9 +443,36 @@ void CFG_setWifi(bool on);
// Default view on boot
int CFG_getDefaultView(void);
void CFG_setDefaultView(int view);
+// Game/system list layout: CONTENT_LAYOUT_LIST or CONTENT_LAYOUT_GRID
+int CFG_getContentLayout(void);
+void CFG_setContentLayout(int layout);
+// Number of tile rows in grid layout (GRID_ROWS_MIN..GRID_ROWS_MAX)
+int CFG_getGridRows(void);
+void CFG_setGridRows(int rows);
+// Screen margin in unscaled px (0..SCREEN_MARGIN_MAX), or SCREEN_MARGIN_DEFAULT
+// for the platform's PADDING. Use GFX_getScreenMargin() for the scaled value.
+int CFG_getScreenMargin(void);
+void CFG_setScreenMargin(int margin);
+// Swap the game list's A and X: A resumes the last save state (when there is one)
+// and X starts the game fresh, instead of A open / X resume.
+bool CFG_getResumeWithA(void);
+void CFG_setResumeWithA(bool on);
+// Skip the system list and show every game on one page, sorted A-Z
+bool CFG_getAllGamesView(void);
+void CFG_setAllGamesView(bool on);
// Quick switcher UI painting on/off
bool CFG_getShowQuickswitcherUI(void);
void CFG_setShowQuickswitcherUI(bool on);
+// Quick Menu title pill (selected item's name) on/off
+bool CFG_getShowQuickMenuTitle(void);
+void CFG_setShowQuickMenuTitle(bool on);
+// Quick Menu top row entries on/off (Recents, Games, Tools)
+bool CFG_getQuickMenuRecents(void);
+void CFG_setQuickMenuRecents(bool on);
+bool CFG_getQuickMenuGames(void);
+void CFG_setQuickMenuGames(bool on);
+bool CFG_getQuickMenuTools(void);
+void CFG_setQuickMenuTools(bool on);
// WiFi diagnostic logging on/off
bool CFG_getWifiDiagnostics(void);
void CFG_setWifiDiagnostics(bool on);
diff --git a/workspace/all/minarch/ma_menu.c b/workspace/all/minarch/ma_menu.c
index 85ca4341aa..d2f7f9558f 100644
--- a/workspace/all/minarch/ma_menu.c
+++ b/workspace/all/minarch/ma_menu.c
@@ -113,7 +113,7 @@ void MSG_quit(void) {
///////////////////////////////////////
-#define MENU_ITEM_COUNT 5
+#define MENU_ITEM_COUNT 6
#define MENU_SLOT_COUNT 8
enum {
@@ -121,6 +121,7 @@ enum {
ITEM_SAVE,
ITEM_LOAD,
ITEM_OPTS,
+ ITEM_SAVE_QUIT, // only shown when Menu_itemVisible() says so
ITEM_QUIT,
};
@@ -162,10 +163,29 @@ static struct {
[ITEM_SAVE] = "Save",
[ITEM_LOAD] = "Load",
[ITEM_OPTS] = "Options",
+ [ITEM_SAVE_QUIT] = "Save & Quit",
[ITEM_QUIT] = "Quit",
}
};
+// Save & Quit is opt-in (Settings > System) and only offered once this session
+// has a slot to save into: one it resumed from, or saved/loaded during play.
+// Hidden while save states are blocked (RetroAchievements hardcore mode).
+static int Menu_itemVisible(int item) {
+ if (item == ITEM_SAVE_QUIT)
+ return CFG_getSaveQuitMenu() && session_slot >= 0 && session_slot < USER_STATE_SLOTS && !RA_isHardcoreModeActive();
+ return 1;
+}
+
+// Steps selected by dir (+1/-1) to the next visible item, wrapping around.
+static int Menu_stepItem(int selected, int dir) {
+ for (int n = 0; n < MENU_ITEM_COUNT; n++) {
+ selected = (selected + dir + MENU_ITEM_COUNT) % MENU_ITEM_COUNT;
+ if (Menu_itemVisible(selected)) break;
+ }
+ return selected;
+}
+
void Menu_init(void) {
menu.overlay = SDL_CreateRGBSurfaceWithFormat(SDL_SWSURFACE,
DEVICE_WIDTH,DEVICE_HEIGHT,
@@ -1688,7 +1708,7 @@ void Menu_screenshot(void) {
Notification_push(NOTIFICATION_SETTING, "Screenshot saved", NULL);
}
}
-void Menu_saveState(void) {
+int Menu_saveState(void) {
// LOG_info("Menu_saveState\n");
Menu_updateState();
@@ -1718,7 +1738,8 @@ void Menu_saveState(void) {
state_slot = menu.slot;
putInt(menu.slot_path, menu.slot);
int success = State_write();
-
+ if (success) session_slot = menu.slot;
+
// Show notification if enabled
if (CFG_getNotifyManualSave()) {
char msg[NOTIFICATION_MAX_MESSAGE];
@@ -1726,6 +1747,7 @@ void Menu_saveState(void) {
snprintf(msg, sizeof(msg), success ? "State Saved - Slot %d" : "Save Failed - Slot %d", menu.slot + 1);
Notification_push(NOTIFICATION_SAVE_STATE, msg, NULL);
}
+ return success;
}
void Menu_loadState(void) {
Menu_updateState();
@@ -1748,8 +1770,9 @@ void Menu_loadState(void) {
state_slot = menu.slot;
putInt(menu.slot_path, menu.slot);
int success = State_read();
+ if (success) session_slot = menu.slot;
Rewind_on_state_change();
-
+
// Show notification if enabled
if (CFG_getNotifyLoad()) {
char msg[NOTIFICATION_MAX_MESSAGE];
@@ -1837,13 +1860,11 @@ void Menu_loop(void) {
PAD_poll();
if (PAD_justPressed(BTN_UP)) {
- selected -= 1;
- if (selected<0) selected += MENU_ITEM_COUNT;
+ selected = Menu_stepItem(selected, -1);
dirty = 1;
}
else if (PAD_justPressed(BTN_DOWN)) {
- selected += 1;
- if (selected>=MENU_ITEM_COUNT) selected -= MENU_ITEM_COUNT;
+ selected = Menu_stepItem(selected, 1);
dirty = 1;
}
else if (PAD_justPressed(BTN_LEFT)) {
@@ -1931,6 +1952,17 @@ void Menu_loop(void) {
}
}
break;
+ case ITEM_SAVE_QUIT:
+ // save into this session's slot (not whichever slot Save/Load is
+ // currently browsing), and only quit once that worked
+ menu.slot = session_slot;
+ if (Menu_saveState()) {
+ status = STATUS_QUIT;
+ show_menu = 0;
+ quit = 1;
+ }
+ else dirty = 1;
+ break;
case ITEM_QUIT:
status = STATUS_QUIT;
show_menu = 0;
@@ -1979,48 +2011,63 @@ void Menu_loop(void) {
GFX_blitButtonGroup((char*[]){ "B","BACK", "A","OKAY", NULL }, 1, screen, 1);
// list
- oy = (((DEVICE_HEIGHT / FIXED_SCALE) - PADDING * 2) - (MENU_ITEM_COUNT * PILL_SIZE)) / 2;
- for (int i=0; i max_list_h) row_h = MAX(1, max_list_h / visible_items);
+ oy = (list_h - (visible_items * row_h)) / 2;
+
+ for (int i=0, row=0; i1 && i==ITEM_CONT) {
+ if (menu.total_discs>1 && i==ITEM_CONT) {
GFX_blitPillDark(ASSET_WHITE_PILL, screen, &(SDL_Rect){
SCALE1(PADDING),
- SCALE1(oy + PADDING),
+ pill_y,
screen->w - SCALE1(PADDING * 2),
SCALE1(PILL_SIZE)
});
text = TTF_RenderUTF8_Blended(font.large, disc_name, text_color);
SDL_BlitSurface(text, NULL, screen, &(SDL_Rect){
screen->w - SCALE1(PADDING + BUTTON_PADDING) - text->w,
- SCALE1(oy + PADDING + 4)
+ pill_y + SCALE1(4)
});
SDL_FreeSurface(text);
}
-
+
TTF_SizeUTF8(font.large, item, &ow, NULL);
ow += SCALE1(BUTTON_PADDING*2);
-
+
// pill
GFX_blitPillDark(ASSET_WHITE_PILL, screen, &(SDL_Rect){
SCALE1(PADDING),
- SCALE1(oy + PADDING + (i * PILL_SIZE)),
+ pill_y,
ow,
SCALE1(PILL_SIZE)
});
}
-
-
+
+
// text
text = TTF_RenderUTF8_Blended(font.large, item, text_color);
SDL_BlitSurface(text, NULL, screen, &(SDL_Rect){
SCALE1(PADDING + BUTTON_PADDING),
- SCALE1(oy + PADDING + (i * PILL_SIZE) + 4)
+ pill_y + SCALE1(4)
});
SDL_FreeSurface(text);
}
diff --git a/workspace/all/minarch/ma_menu.h b/workspace/all/minarch/ma_menu.h
index 749a81b663..51c7f28340 100644
--- a/workspace/all/minarch/ma_menu.h
+++ b/workspace/all/minarch/ma_menu.h
@@ -8,7 +8,7 @@ void Menu_beforeSleep(void);
void Menu_afterSleep(void);
int Menu_options(MenuList* list);
void Menu_screenshot(void);
-void Menu_saveState(void);
+int Menu_saveState(void); // returns 1 if the state was written
void Menu_loadState(void);
void OptionSaveChanges_updateDesc(void);
void OptionAchievements_updateDesc(void);
diff --git a/workspace/all/minarch/ma_saves.c b/workspace/all/minarch/ma_saves.c
index 8fe053e012..d277506e8a 100644
--- a/workspace/all/minarch/ma_saves.c
+++ b/workspace/all/minarch/ma_saves.c
@@ -157,6 +157,7 @@ void RTC_write(void) {
///////////////////////////////////////
int state_slot = 0;
+int session_slot = -1;
void State_getPath(char* filename) {
char work_name[MAX_PATH];
@@ -378,9 +379,13 @@ void State_resume(void) {
if (!exists(RESUME_SLOT_PATH)) return;
int last_state_slot = state_slot;
- state_slot = getInt(RESUME_SLOT_PATH);
+ int resume_slot = getInt(RESUME_SLOT_PATH);
+ state_slot = resume_slot;
unlink(RESUME_SLOT_PATH);
- State_read();
+ // resuming a user slot (not the hidden default or auto-resume slot) makes it
+ // this session's slot, eg. the target of the menu's Save & Quit
+ if (State_read() && resume_slot >= 0 && resume_slot < USER_STATE_SLOTS)
+ session_slot = resume_slot;
state_slot = last_state_slot;
Rewind_on_state_change();
}
diff --git a/workspace/all/minarch/ma_saves.h b/workspace/all/minarch/ma_saves.h
index 0d8882dc72..830adf91a5 100644
--- a/workspace/all/minarch/ma_saves.h
+++ b/workspace/all/minarch/ma_saves.h
@@ -2,6 +2,13 @@
extern int state_slot;
+// User-facing state slots 1-8 are internal 0-7; 8 is the hidden default state
+// and AUTO_RESUME_SLOT (9) the auto save.
+#define USER_STATE_SLOTS 8
+// The user slot this session resumed from, or last saved/loaded successfully,
+// -1 if none yet. Target of the in-game menu's Save & Quit.
+extern int session_slot;
+
void SRAM_read(void);
void SRAM_write(void);
void RTC_read(void);
diff --git a/workspace/all/nextui/nextui.c b/workspace/all/nextui/nextui.c
index 90c5673902..4a67f0db15 100644
--- a/workspace/all/nextui/nextui.c
+++ b/workspace/all/nextui/nextui.c
@@ -215,8 +215,14 @@ typedef struct Directory {
int selected;
int start;
int end;
+ int grid_col; // first visible column in grid layout
} Directory;
+// "All games" root: when enabled, the root directory lists every game of every
+// system (A-Z) instead of the system list. all_games_root is set by getRoot()
+// each time the root is built.
+static int all_games_root = 0;
+
static int getIndexChar(char* str) {
char i = 0;
char c = tolower(str[0]);
@@ -243,6 +249,26 @@ static void Directory_index(Directory* self) {
int is_collection = prefixMatch(COLLECTIONS_PATH, self->path);
int skip_index = exactMatch(FAUX_RECENT_PATH, self->path) || is_collection; // not alphabetized
+ if (all_games_root && exactMatch(self->path, SDCARD_PATH)) {
+ // map.txt aliases were already applied per system by getAllGames(), and
+ // duplicate names across systems are intentionally left untagged.
+ int alpha = -1;
+ int index = 0;
+ for (int i = 0; i < self->entries->count; i++) {
+ Entry* entry = self->entries->items[i];
+ int a = getIndexChar(entry->name);
+ // a merged list can contain more than INT_ARRAY_MAX index changes
+ // (eg. symbols sorting after letters), keep the remainder in the last group
+ if (a != alpha && self->alphas->count < INT_ARRAY_MAX) {
+ index = self->alphas->count;
+ IntArray_push(self->alphas, i);
+ alpha = a;
+ }
+ entry->alpha = index;
+ }
+ return;
+ }
+
Hash* map = NULL;
char map_path[256];
sprintf(map_path, "%s/map.txt", is_collection ? COLLECTIONS_PATH : self->path);
@@ -384,6 +410,9 @@ static Directory* Directory_new(char* path, int selected) {
}
self->alphas = IntArray_new();
self->selected = selected;
+ self->start = 0;
+ self->end = 0;
+ self->grid_col = 0;
Directory_index(self);
return self;
}
@@ -699,12 +728,30 @@ static int hasRoms(char* dir_name) {
return has;
}
+static void getToolsPath(char* out, size_t out_size) {
+ snprintf(out, out_size, "%s/Tools/%s", SDCARD_PATH, PLATFORM);
+}
+
+static int isToolsEntry(Entry* entry) {
+ char tools_path[256];
+ getToolsPath(tools_path, sizeof(tools_path));
+ return exactMatch(entry->path, tools_path);
+}
+
static int hasTools(void) {
char tools_path[256];
- snprintf(tools_path, sizeof(tools_path), "%s/Tools/%s", SDCARD_PATH, PLATFORM);
+ getToolsPath(tools_path, sizeof(tools_path));
return exists(tools_path);
}
+// The Tools folder entry for a root page, when it should be listed there.
+static void pushToolsEntry(Array* root) {
+ if (!hasTools() || !CFG_getShowTools() || simple_mode) return;
+ char tools_path[256];
+ getToolsPath(tools_path, sizeof(tools_path));
+ Array_push(root, Entry_new(tools_path, ENTRY_DIR));
+}
+
static Array* getRoms()
{
Array* entries = Array_new();
@@ -807,26 +854,39 @@ static Array* getCollections(void)
static Array* getQuickEntries(void) {
Array* entries = Array_new();
+ // Recents, Games and Tools can each be hidden in Settings > Appearance; the
+ // top row may then be empty (see the Quick Menu's qm_row handling)
+
// We assume Menu_init was already called and populated this
- if (recents && recents->count)
+ if (recents && recents->count && CFG_getQuickMenuRecents())
Array_push(entries, Entry_newNamed(FAUX_RECENT_PATH, ENTRY_DIR, "Recents"));
if (hasCollections())
Array_push(entries, Entry_new(COLLECTIONS_PATH, ENTRY_DIR));
// Not sure we need this, its just a button press away (B)
- Array_push(entries, Entry_newNamed(ROMS_PATH, ENTRY_DIR, "Games"));
+ if (CFG_getQuickMenuGames())
+ Array_push(entries, Entry_newNamed(ROMS_PATH, ENTRY_DIR, "Games"));
// Add tools if applicable
- if (hasTools() && !simple_mode) {
+ if (hasTools() && !simple_mode && CFG_getQuickMenuTools()) {
char tools_path[256];
- snprintf(tools_path, sizeof(tools_path), "%s/Tools/%s", SDCARD_PATH, PLATFORM);
+ getToolsPath(tools_path, sizeof(tools_path));
Array_push(entries, Entry_new(tools_path, ENTRY_DIR));
}
return entries;
}
+// Display name for a Quick Menu item, shown in the title pill. Toggles are
+// identified by their internal names (see toggleQuick), so only relabel here.
+static const char* QuickMenu_label(Entry* item) {
+ if (!item) return "";
+ if (!strcmp(item->name, "Wifi")) return "Wi-Fi";
+ if (!strcmp(item->name, "Poweroff")) return "Power off";
+ return item->name;
+}
+
static Array* getQuickToggles(void) {
Array *entries = Array_new();
@@ -851,7 +911,89 @@ static Array* getQuickToggles(void) {
return entries;
}
+// Applies /map.txt display-name aliases to entries the same way
+// Directory_index() does for a single folder: renames, drops entries whose
+// alias marks them hidden, and resorts if anything was renamed.
+static Array* applyDirMap(Array* entries, const char* dir_path) {
+ char map_path[256];
+ snprintf(map_path, sizeof(map_path), "%s/map.txt", dir_path);
+ if (!exists(map_path)) return entries;
+
+ FILE* file = fopen(map_path, "r");
+ if (!file) return entries;
+
+ Hash* map = Hash_new();
+ char line[256];
+ while (fgets(line, sizeof(line), file) != NULL) {
+ normalizeNewline(line);
+ trimTrailingNewlines(line);
+ if (strlen(line) == 0) continue;
+
+ char* tmp = strchr(line, '\t');
+ if (tmp) {
+ tmp[0] = '\0';
+ Hash_set(map, line, strdup(tmp + 1));
+ }
+ }
+ fclose(file);
+
+ int resort = 0;
+ Array* kept = Array_new();
+ for (int i = 0; i < entries->count; i++) {
+ Entry* entry = entries->items[i];
+ char* filename = strrchr(entry->path, '/') + 1;
+ char* alias = Hash_get(map, filename);
+ if (alias) {
+ free(entry->name);
+ entry->name = strdup(alias);
+ resort = 1;
+ }
+ if (hide(entry->name)) Entry_free(entry);
+ else Array_push(kept, entry);
+ }
+ Array_free(entries);
+ Hash_free(map);
+
+ if (resort) EntryArray_sort(kept);
+ return kept;
+}
+
+// Every game of every visible system, sorted A-Z, for the "All games" root page.
+// Systems come from getRoms() so hidden/empty systems and collation of sibling
+// folders match the regular system list.
+// Subfolders inside a system (multi-disc games, user folders) stay as folders.
+static Array* getAllGames(void) {
+ Array* all = Array_new();
+ Array* systems = getRoms();
+
+ for (int i = 0; i < systems->count; i++) {
+ Entry* system = systems->items[i];
+ Array* games = applyDirMap(getEntries(system->path), system->path);
+ Array_yoink(all, games);
+ }
+ EntryArray_free(systems);
+
+ EntryArray_sort(all);
+ return all;
+}
+
static Array* getRoot(void) {
+ if (CFG_getAllGamesView()) {
+ // hasRecents() (re)loads the recents list the Quick Menu and game switcher
+ // rely on, and consumes a pending disc change, so it must still run here
+ hasRecents();
+ Array* all = getAllGames();
+ if (all->count > 0) {
+ all_games_root = 1;
+ // after the A-Z games, same as the end of the system list
+ pushToolsEntry(all);
+ return all;
+ }
+ // no games anywhere: fall back to the regular root so Tools etc. stay reachable
+ EntryArray_free(all);
+ }
+ all_games_root = 0;
+
Array* root = Array_new();
if (hasRecents() && CFG_getShowRecents())
@@ -873,11 +1015,7 @@ static Array* getRoot(void) {
Array_yoink(root, entries);
// Add tools if applicable
- if (hasTools() && CFG_getShowTools() && !simple_mode) {
- char tools_path[256];
- snprintf(tools_path, sizeof(tools_path), "%s/Tools/%s", SDCARD_PATH, PLATFORM);
- Array_push(root, Entry_new(tools_path, ENTRY_DIR));
- }
+ pushToolsEntry(root);
return root;
}
@@ -1585,6 +1723,31 @@ static void loadLast(void) { // call after loading root directory
char full_path[256];
strcpy(full_path, last_path);
+ // "All games" root: games sit directly at the root, so select the game (or the
+ // game's folder, eg. multi-disc) instead of walking Roms//...
+ if (all_games_root && stack->count == 1 && prefixMatch(ROMS_PATH "/", full_path)) {
+ for (int i = 0; i < top->entries->count; i++) {
+ Entry* entry = top->entries->items[i];
+ size_t len = strlen(entry->path);
+ if (exactMatch(entry->path, full_path) || (prefixMatch(entry->path, full_path) && full_path[len] == '/')) {
+ top->selected = i;
+ if (i >= top->end) {
+ top->start = i;
+ top->end = top->start + MAIN_ROW_COUNT;
+ if (top->end > top->entries->count) {
+ top->end = top->entries->count;
+ top->start = top->end - MAIN_ROW_COUNT;
+ if (top->start < 0) top->start = 0;
+ }
+ }
+ break;
+ }
+ }
+ if (top->selected >= 0 && top->selected < top->entries->count)
+ readyResume(top->entries->items[top->selected]);
+ return;
+ }
+
char* tmp;
char filename[256];
tmp = strrchr(last_path, '/');
@@ -1598,6 +1761,17 @@ static void loadLast(void) { // call after loading root directory
last_path[(slash-last_path)] = '\0';
}
+ // "All games" root has no Recents/Collections entries (they live in the Quick
+ // Menu), so open that top-level folder directly and restore within it.
+ if (all_games_root && stack->count == 1 && last->count > 0) {
+ char* first = last->items[last->count - 1];
+ if (exactMatch(first, FAUX_RECENT_PATH) || exactMatch(first, COLLECTIONS_PATH)) {
+ char* path = Array_pop(last);
+ openDirectory(path, 0);
+ free(path);
+ }
+ }
+
while (last->count>0) {
char* path = Array_pop(last);
if (!exactMatch(path, ROMS_PATH)) { // romsDir is effectively root as far as restoring state after a game
@@ -1776,6 +1950,418 @@ static inline int getAnimationDraw(void) { return SDL_AtomicGet(&animationDrawAt
static inline void setNeedDraw(int v) { SDL_AtomicSet(&needDrawAtomic, v); }
static inline int getNeedDraw(void) { return SDL_AtomicGet(&needDrawAtomic); }
+///////////////////////////////////////
+// Grid layout: tiles laid out column-major (top to bottom, then the next column)
+// so the content scrolls horizontally. Up/Down step through tiles in order
+// (wrapping into the neighbouring column at a column's ends), Left/Right move
+// between columns. Used for every game list screen when the content layout
+// setting is "Grid"; the list renderer is untouched otherwise.
+
+typedef struct GridLayout {
+ int rows;
+ int cols; // visible columns
+ int tile_w;
+ int tile_h;
+ int gap;
+ int x; // left edge of the first visible column
+ int y; // top edge of the first row
+} GridLayout;
+
+static int Grid_active(void) {
+ return CFG_getContentLayout() == CONTENT_LAYOUT_GRID;
+}
+
+static void Grid_getLayout(GridLayout* g) {
+ // leave the top row for the title pill and hardware status, the bottom row for button hints
+ int margin = GFX_getScreenMargin();
+ int top = margin + SCALE1(PILL_SIZE + PADDING);
+ int bottom = screen->h - margin - SCALE1(PILL_SIZE + PADDING);
+ int area_w = screen->w - margin * 2;
+ int area_h = bottom - top;
+
+ g->gap = SCALE1(BUTTON_MARGIN * 2);
+ g->rows = clamp(CFG_getGridRows(), GRID_ROWS_MIN, GRID_ROWS_MAX);
+ g->tile_h = (area_h - g->gap * (g->rows - 1)) / g->rows;
+ g->tile_w = g->tile_h;
+
+ // keep at least a few columns visible so horizontal scrolling reads as a grid,
+ // tiles become portrait (box art shaped) when rows are tall
+ const int min_cols = 3;
+ int max_tile_w = (area_w - g->gap * (min_cols - 1)) / min_cols;
+ if (g->tile_w > max_tile_w) g->tile_w = max_tile_w;
+
+ g->cols = (area_w + g->gap) / (g->tile_w + g->gap);
+ if (g->cols < 1) g->cols = 1;
+
+ int used_w = g->cols * g->tile_w + (g->cols - 1) * g->gap;
+ g->x = (screen->w - used_w) / 2;
+ g->y = top;
+}
+
+// Returns the new selection after moving by (dx, dy) tiles. Up/Down step through
+// the tiles in order, so the top of a column continues to the bottom of the
+// previous one and the bottom continues to the top of the next. wrap allows
+// jumping from the first tile/column to the last (and back), only used on a fresh
+// press so holding the d-pad stops at the edges, same as the list.
+static int Grid_move(int selected, int total, int rows, int dx, int dy, int wrap) {
+ if (total <= 0) return 0;
+ int row = selected % rows;
+ int col = selected / rows;
+ int last_col = (total - 1) / rows;
+
+ if (dy < 0) {
+ if (selected > 0) return selected - 1;
+ return wrap ? total - 1 : selected;
+ }
+ if (dy > 0) {
+ if (selected + 1 < total) return selected + 1;
+ return wrap ? 0 : selected;
+ }
+
+ if (dx > 0) {
+ if (col < last_col) return MIN(selected + rows, total - 1);
+ return wrap ? MIN(row, total - 1) : selected;
+ }
+ if (dx < 0) {
+ if (col > 0) return selected - rows;
+ return wrap ? MIN(last_col * rows + row, total - 1) : selected;
+ }
+ return selected;
+}
+
+// Keeps the selected tile's column on screen.
+static void Grid_scrollTo(Directory* dir, const GridLayout* g) {
+ int total_cols = (dir->entries->count + g->rows - 1) / g->rows;
+ int col = dir->selected / g->rows;
+ if (col < dir->grid_col) dir->grid_col = col;
+ if (col >= dir->grid_col + g->cols) dir->grid_col = col - g->cols + 1;
+ dir->grid_col = clamp(dir->grid_col, 0, MAX(0, total_cols - g->cols));
+}
+
+// Tile art uses the same files as game art: /.media/.png.
+// Folders (eg. systems) fall back to their own .media/bg.png.
+static int Grid_findArt(Entry* entry, char* out, size_t out_size) {
+ char path_copy[MAX_PATH];
+ strncpy(path_copy, entry->path, sizeof(path_copy) - 1);
+ path_copy[sizeof(path_copy) - 1] = '\0';
+
+ char* name = strrchr(path_copy, '/');
+ if (!name) return 0;
+ *name = '\0';
+ name++;
+
+ char base[MAX_PATH];
+ strncpy(base, name, sizeof(base) - 1);
+ base[sizeof(base) - 1] = '\0';
+ if (entry->type != ENTRY_DIR) {
+ char* dot = strrchr(base, '.');
+ if (dot) *dot = '\0';
+ }
+
+ snprintf(out, out_size, "%s/.media/%s.png", path_copy, base);
+ if (exists(out)) return 1;
+
+ if (entry->type == ENTRY_DIR) {
+ snprintf(out, out_size, "%s/.media/bg.png", entry->path);
+ if (exists(out)) return 1;
+ }
+ return 0;
+}
+
+// Tile corner radius: the "Game art corner radius" setting, so tiles match game art
+// elsewhere in the UI, limited on small tiles so they don't turn into circles.
+static int Grid_cornerRadius(int w, int h) {
+ return MAX(0, MIN(SCALE1(CFG_getThumbnailRadius()), MIN(w, h) / 4));
+}
+
+// Fills rect on screen with color and rounds its corners (pixels outside the
+// radius become transparent, letting the background layer show through).
+static void Grid_fillRounded(SDL_Rect* rect, Uint32 color, int radius) {
+ SDL_FillRect(screen, rect, color);
+ if (radius <= 0) return;
+
+ // GFX_ApplyRoundedCorners_8888 writes pixels directly, keep it on-surface
+ SDL_Rect clipped;
+ if (!SDL_IntersectRect(rect, &(SDL_Rect){ 0, 0, screen->w, screen->h }, &clipped)) return;
+ if (clipped.w != rect->w || clipped.h != rect->h) return; // partially off-screen, leave square
+ GFX_ApplyRoundedCorners_8888(screen, rect, radius);
+}
+
+static SDL_Surface* Grid_loadArt(const char* path, int max_w, int max_h) {
+ SDL_Surface* image = IMG_Load(path);
+ if (!image) return NULL;
+ SDL_Surface* src = SDL_ConvertSurfaceFormat(image, screen->format->format, 0);
+ SDL_FreeSurface(image);
+ if (!src) return NULL;
+
+ double scale = MIN((double)max_w / src->w, (double)max_h / src->h);
+ int w = MAX(1, (int)(src->w * scale));
+ int h = MAX(1, (int)(src->h * scale));
+
+ SDL_Surface* dst = SDL_CreateRGBSurfaceWithFormat(0, w, h, screen->format->BitsPerPixel, screen->format->format);
+ if (dst) {
+ SDL_FillRect(dst, NULL, 0);
+ SDL_SetSurfaceBlendMode(src, SDL_BLENDMODE_NONE);
+#if SDL_VERSION_ATLEAST(2, 0, 16)
+ if (SDL_SoftStretchLinear(src, NULL, dst, NULL) != 0)
+#endif
+ SDL_BlitScaled(src, NULL, dst, NULL);
+
+ int radius = MIN(Grid_cornerRadius(max_w, max_h), MIN(w, h) / 2);
+ if (radius > 0)
+ GFX_ApplyRoundedCorners_8888(dst, &(SDL_Rect){0, 0, w, h}, radius);
+ SDL_SetSurfaceBlendMode(dst, SDL_BLENDMODE_BLEND);
+ }
+ SDL_FreeSurface(src);
+ return dst;
+}
+
+// Small LRU cache of scaled tile art keyed by entry path and tile size. Entries
+// without art are cached too (surface == NULL) so they aren't probed every frame.
+#define GRID_ART_CACHE_SIZE 48
+typedef struct GridArt {
+ char key[MAX_PATH];
+ int w;
+ int h;
+ SDL_Surface* surface;
+ unsigned int used;
+} GridArt;
+static GridArt grid_art[GRID_ART_CACHE_SIZE];
+static unsigned int grid_art_clock = 0;
+
+static SDL_Surface* Grid_getArt(Entry* entry, int max_w, int max_h) {
+ int victim = -1;
+ for (int i = 0; i < GRID_ART_CACHE_SIZE; i++) {
+ GridArt* slot = &grid_art[i];
+ if (!slot->key[0]) {
+ if (victim < 0 || grid_art[victim].key[0]) victim = i; // prefer an empty slot
+ continue;
+ }
+ if (slot->w == max_w && slot->h == max_h && exactMatch(slot->key, entry->path)) {
+ slot->used = ++grid_art_clock;
+ return slot->surface;
+ }
+ if (victim < 0 || (grid_art[victim].key[0] && slot->used < grid_art[victim].used)) victim = i;
+ }
+
+ GridArt* slot = &grid_art[victim];
+ if (slot->surface) SDL_FreeSurface(slot->surface);
+ slot->surface = NULL;
+
+ char art_path[MAX_PATH];
+ if (Grid_findArt(entry, art_path, sizeof(art_path)))
+ slot->surface = Grid_loadArt(art_path, max_w, max_h);
+
+ strncpy(slot->key, entry->path, sizeof(slot->key) - 1);
+ slot->key[sizeof(slot->key) - 1] = '\0';
+ slot->w = max_w;
+ slot->h = max_h;
+ slot->used = ++grid_art_clock;
+ return slot->surface;
+}
+
+// The Tools tile (system list and all games view) shows the Quick Menu's Tools
+// icon instead of a letter when it has no art of its own. Loaded from the largest
+// asset and scaled down to the tile, kept while the size is unchanged.
+static SDL_Surface* grid_tools_icon = NULL;
+static int grid_tools_icon_px = 0;
+
+static SDL_Surface* Grid_getToolsIcon(int px) {
+ if (grid_tools_icon_px == px) return grid_tools_icon;
+ if (grid_tools_icon) SDL_FreeSurface(grid_tools_icon);
+ grid_tools_icon = NULL;
+ grid_tools_icon_px = px;
+
+ SDL_Surface* image = IMG_Load(SDCARD_PATH "/.system/res/Tools@4x.png");
+ if (!image) return NULL;
+ SDL_Surface* src = SDL_ConvertSurfaceFormat(image, screen->format->format, 0);
+ SDL_FreeSurface(image);
+ if (!src) return NULL;
+
+ SDL_Surface* dst = SDL_CreateRGBSurfaceWithFormat(0, px, px, screen->format->BitsPerPixel, screen->format->format);
+ if (dst) {
+ SDL_FillRect(dst, NULL, 0);
+ SDL_SetSurfaceBlendMode(src, SDL_BLENDMODE_NONE);
+#if SDL_VERSION_ATLEAST(2, 0, 16)
+ if (SDL_SoftStretchLinear(src, NULL, dst, NULL) != 0)
+#endif
+ SDL_BlitScaled(src, NULL, dst, NULL);
+ SDL_SetSurfaceBlendMode(dst, SDL_BLENDMODE_BLEND);
+ }
+ SDL_FreeSurface(src);
+ grid_tools_icon = dst;
+ return grid_tools_icon;
+}
+
+// Tiles without art show the first letter of the name, rendered with the UI font
+// opened at a size that suits the tile (kept open while the tile size is unchanged).
+static TTF_Font* grid_letter_font = NULL;
+static int grid_letter_font_px = 0;
+
+static TTF_Font* Grid_getLetterFont(int px) {
+ if (grid_letter_font && grid_letter_font_px == px) return grid_letter_font;
+
+ const char* path = GFX_getSystemFontPath();
+ TTF_Font* opened = (path && path[0]) ? TTF_OpenFont(path, px) : NULL;
+ if (!opened) return font.large;
+ TTF_SetFontStyle(opened, CFG_getFontStyle());
+
+ if (grid_letter_font) TTF_CloseFont(grid_letter_font);
+ grid_letter_font = opened;
+ grid_letter_font_px = px;
+ return grid_letter_font;
+}
+
+// First letter or digit of name (skipping leading punctuation like "[" or "'"),
+// uppercased. Non-ASCII characters are copied whole as UTF-8. out needs 5 bytes.
+static void Grid_getInitial(const char* name, char* out) {
+ const unsigned char* pick = NULL;
+ for (const unsigned char* c = (const unsigned char*)name; *c; c++) {
+ if (*c >= 0xC0 || isalnum(*c)) { pick = c; break; }
+ }
+ if (!pick) pick = (const unsigned char*)name;
+ if (!pick[0]) { strcpy(out, "?"); return; }
+
+ int len = 1;
+ if (pick[0] >= 0xF0) len = 4;
+ else if (pick[0] >= 0xE0) len = 3;
+ else if (pick[0] >= 0xC0) len = 2;
+
+ int i = 0;
+ for (; i < len && pick[i]; i++) out[i] = pick[i];
+ out[i] = '\0';
+ if (len == 1) out[0] = toupper((unsigned char)out[0]);
+}
+
+static void Grid_freeArt(void) {
+ for (int i = 0; i < GRID_ART_CACHE_SIZE; i++) {
+ if (grid_art[i].surface) SDL_FreeSurface(grid_art[i].surface);
+ grid_art[i].surface = NULL;
+ grid_art[i].key[0] = '\0';
+ }
+ if (grid_letter_font) TTF_CloseFont(grid_letter_font);
+ grid_letter_font = NULL;
+ grid_letter_font_px = 0;
+ if (grid_tools_icon) SDL_FreeSurface(grid_tools_icon);
+ grid_tools_icon = NULL;
+ grid_tools_icon_px = 0;
+}
+
+// Draws label in a pill in the top-left corner, eg. the selected grid tile or
+// Quick Menu item. It sits the screen margin away from the edges, like the status
+// pill and button hints, and always keeps one margin of space before the status
+// pill on the right (ow is that pill's width, from GFX_blitHardwareGroup).
+static void drawTitlePill(const char* label, int ow) {
+ if (!label || !label[0]) return;
+
+ int margin = GFX_getScreenMargin();
+ char display_name[256];
+ int max_width = MAX(0, screen->w - margin * 3 - ow);
+ SDL_LockMutex(fontMutex);
+ int text_width = GFX_getTextWidth(font.large, label, display_name, max_width, SCALE1(BUTTON_PADDING * 2));
+ int pill_width = MIN(max_width, text_width);
+ if (pill_width > 0)
+ GFX_blitPillDark(ASSET_WHITE_PILL, screen, &(SDL_Rect){ margin, margin, pill_width, SCALE1(PILL_SIZE) });
+ SDL_Surface* text = TTF_RenderUTF8_Blended(font.large, display_name, uintToColour(THEME_COLOR5_255));
+ SDL_UnlockMutex(fontMutex);
+ int text_w = pill_width - SCALE1(BUTTON_PADDING * 2);
+ if (text && text_w > 0) {
+ int text_offset_y = (SCALE1(PILL_SIZE) - text->h + 1) >> 1;
+ SDL_BlitSurface(text, &(SDL_Rect){ 0, 0, text_w, text->h }, screen,
+ &(SDL_Rect){ margin + SCALE1(BUTTON_PADDING), margin + text_offset_y });
+ }
+ if (text) SDL_FreeSurface(text);
+}
+
+// Draws the visible tiles of dir onto screen, plus a title pill with the selected
+// entry's name in the top row (when names are shown). ow is the width taken by
+// the hardware status group in the top-right corner.
+static void Grid_render(Directory* dir, int show_names, int ow) {
+ GridLayout g;
+ Grid_getLayout(&g);
+ Grid_scrollTo(dir, &g);
+
+ int total = dir->entries->count;
+ int first = dir->grid_col * g.rows;
+ int last = MIN(total, first + g.cols * g.rows);
+ int border = SCALE1(2);
+
+ SDL_Color text_color = uintToColour(THEME_COLOR4_255);
+ SDL_Color selected_text_color = uintToColour(THEME_COLOR5_255);
+ SDL_Color tile_rgb = uintToColour(THEME_COLOR3_255);
+ Uint32 tile_fill = SDL_MapRGBA(screen->format, tile_rgb.r, tile_rgb.g, tile_rgb.b, 0xC0);
+
+ // art-less tiles show a single large initial; no wrapping or "..." truncation,
+ // which GFX_truncateText can't do safely in very narrow tiles
+ SDL_LockMutex(fontMutex);
+ TTF_Font* letter_font = Grid_getLetterFont(MAX(1, MIN(g.tile_w, g.tile_h) * 55 / 100));
+ SDL_UnlockMutex(fontMutex);
+
+ for (int i = first; i < last; i++) {
+ Entry* entry = dir->entries->items[i];
+ int col = i / g.rows - dir->grid_col;
+ int row = i % g.rows;
+ int is_selected = (i == dir->selected);
+ SDL_Rect tile = {
+ g.x + col * (g.tile_w + g.gap),
+ g.y + row * (g.tile_h + g.gap),
+ g.tile_w,
+ g.tile_h
+ };
+
+ int radius = Grid_cornerRadius(tile.w, tile.h);
+ SDL_Surface* art = Grid_getArt(entry, tile.w, tile.h);
+ if (art) {
+ SDL_Rect art_rect = { tile.x + (tile.w - art->w) / 2, tile.y + (tile.h - art->h) / 2, art->w, art->h };
+ if (is_selected) {
+ // frame follows the art's rounded corners, offset by the border width
+ int art_radius = MIN(radius, MIN(art->w, art->h) / 2);
+ SDL_Rect frame = { art_rect.x - border, art_rect.y - border, art_rect.w + border * 2, art_rect.h + border * 2 };
+ Grid_fillRounded(&frame, THEME_COLOR1, art_radius > 0 ? art_radius + border : 0);
+ }
+ SDL_BlitSurface(art, NULL, screen, &art_rect);
+ continue;
+ }
+
+ // no art: a plain tile with the name's first letter (or the Tools icon)
+ Grid_fillRounded(&tile, is_selected ? THEME_COLOR1 : tile_fill, radius);
+
+ if (isToolsEntry(entry)) {
+ // same share of the tile as the Quick Menu gives its icons (48 of 72)
+ SDL_Surface* icon = Grid_getToolsIcon(MAX(1, MIN(tile.w, tile.h) * 2 / 3));
+ if (icon) {
+ SDL_Rect dst = { tile.x + (tile.w - icon->w) / 2, tile.y + (tile.h - icon->h) / 2, 0, 0 };
+ GFX_blitSurfaceColor(icon, NULL, screen, &dst, is_selected ? THEME_COLOR5 : THEME_COLOR4);
+ continue;
+ }
+ }
+
+ char* label = entry->name;
+ trimSortingMeta(&label);
+ char initial[5];
+ Grid_getInitial(label, initial);
+
+ SDL_LockMutex(fontMutex);
+ SDL_Surface* letter = TTF_RenderUTF8_Blended(letter_font, initial, is_selected ? selected_text_color : text_color);
+ SDL_UnlockMutex(fontMutex);
+ if (letter) {
+ // clip to the tile in case a glyph renders larger than expected
+ SDL_Rect src = { MAX(0, (letter->w - tile.w) / 2), MAX(0, (letter->h - tile.h) / 2), MIN(letter->w, tile.w), MIN(letter->h, tile.h) };
+ SDL_Rect dst = { tile.x + (tile.w - src.w) / 2, tile.y + (tile.h - src.h) / 2, 0, 0 };
+ SDL_BlitSurface(letter, &src, screen, &dst);
+ SDL_FreeSurface(letter);
+ }
+ }
+
+ // title pill with the selected entry's full name
+ if (show_names && dir->selected >= 0 && dir->selected < total) {
+ Entry* entry = dir->entries->items[dir->selected];
+ char* label = entry->unique ? entry->unique : entry->name;
+ trimSortingMeta(&label);
+ drawTitlePill(label, ow);
+ }
+}
+
static void updatePillTextSurface(const char* entry_name, int move_w, SDL_Color text_color) {
int crop_w = move_w - SCALE1(BUTTON_PADDING * 2);
if (crop_w <= 0) return;
@@ -2270,7 +2856,10 @@ int main (int argc, char *argv[]) {
// start my threaded image loader :D
initImageLoaderPool();
Menu_init();
- int qm_row = 0;
+ // the top row can be empty when its entries are hidden in Settings, then the
+ // bottom row (always has Reboot/Poweroff) is the only one
+ int qm_top_row = quick->count > 0 ? 0 : 1;
+ int qm_row = qm_top_row;
int qm_col = 0;
int qm_slot = 0;
int qm_shift = 0;
@@ -2444,7 +3033,7 @@ int main (int argc, char *argv[]) {
}
}
else if(PAD_justPressed(BTN_UP)) {
- if(qm_row == 1) {
+ if(qm_row == 1 && quick->count > 0) {
qm_row = 0;
qm_col = qm_slot + qm_shift;
dirty = 1;
@@ -2497,7 +3086,7 @@ int main (int argc, char *argv[]) {
if (PAD_tappedMenu(now)) {
currentScreen = SCREEN_QUICKMENU;
qm_col = 0;
- qm_row = 0;
+ qm_row = qm_top_row;
qm_shift = 0;
qm_slot = 0;
dirty = 1;
@@ -2509,6 +3098,18 @@ int main (int argc, char *argv[]) {
switcher_selected = 0;
dirty = 1;
}
+ else if (total>0 && Grid_active()) {
+ GridLayout grid;
+ Grid_getLayout(&grid);
+ if (PAD_justRepeated(BTN_UP))
+ selected = Grid_move(selected, total, grid.rows, 0, -1, PAD_justPressed(BTN_UP));
+ else if (PAD_justRepeated(BTN_DOWN))
+ selected = Grid_move(selected, total, grid.rows, 0, 1, PAD_justPressed(BTN_DOWN));
+ else if (PAD_justRepeated(BTN_LEFT))
+ selected = Grid_move(selected, total, grid.rows, -1, 0, PAD_justPressed(BTN_LEFT));
+ else if (PAD_justRepeated(BTN_RIGHT))
+ selected = Grid_move(selected, total, grid.rows, 1, 0, PAD_justPressed(BTN_RIGHT));
+ }
else if (total>0) {
if (PAD_justRepeated(BTN_UP)) {
if (selected==0 && !PAD_justPressed(BTN_UP)) {
@@ -2611,13 +3212,19 @@ int main (int argc, char *argv[]) {
if (dirty && total>0)
readyResume(entry);
- if (total>0 && can_resume && PAD_justReleased(BTN_RESUME)) {
+ // "Resume with A" swaps the two buttons, but only when there is something
+ // to resume; otherwise A opens as usual
+ int resume_with_a = can_resume && CFG_getResumeWithA();
+ int resume_pressed = resume_with_a ? PAD_justPressed(BTN_A) : PAD_justReleased(BTN_RESUME);
+ int open_pressed = resume_with_a ? PAD_justReleased(BTN_RESUME) : PAD_justPressed(BTN_A);
+
+ if (total>0 && can_resume && resume_pressed) {
should_resume = 1;
Entry_open(entry);
dirty = 1;
}
- else if (total>0 && PAD_justPressed(BTN_A)) {
+ else if (total>0 && open_pressed) {
Entry_open(entry);
if(entry->type == ENTRY_DIR) {
animationdirection = SLIDE_LEFT;
@@ -2697,6 +3304,10 @@ int main (int argc, char *argv[]) {
GFX_blitButtonGroup((char*[]){ "B","BACK", "A","OPEN", NULL }, 1, screen, 1);
if(CFG_getShowQuickswitcherUI()) {
+ // the bottom row is icons only, so name the selected item in the top row
+ if (CFG_getShowQuickMenuTitle())
+ drawTitlePill(QuickMenu_label(current), ow);
+
#define MENU_ITEM_SIZE 72 // item size, top line
#define MENU_MARGIN_Y 32 // space between main UI elements and quick menu
#define MENU_MARGIN_X 40 // space between main UI elements and quick menu
@@ -2774,6 +3385,9 @@ int main (int argc, char *argv[]) {
ox = SCALE1(PADDING + MENU_MARGIN_X);
ox += (screen->w - SCALE1(PADDING + MENU_MARGIN_X + MENU_MARGIN_X + PADDING) - SCALE1(quickActions->count * PILL_SIZE) - SCALE1((quickActions->count - 1) * MENU_TOGGLE_MARGIN))/2;
oy = SCALE1(PADDING + PILL_SIZE + BUTTON_MARGIN + MENU_MARGIN_Y + MENU_LINE_MARGIN) + item_size + item_extra_y / 2;
+ // no cards in the top row (all hidden in Settings): center the buttons on screen
+ if (quick->count == 0)
+ oy = (screen->h - SCALE1(PILL_SIZE)) / 2;
for (int c = 0; c < quickActions->count; c++) {
SDL_Rect item_rect = {ox, oy, SCALE1(PILL_SIZE), SCALE1(PILL_SIZE)};
Entry *item = quickActions->items[c];
@@ -2995,7 +3609,8 @@ int main (int argc, char *argv[]) {
static int lastType = -1;
// this is only a choice on the root folder
- list_show_entry_names = stack->count > 1 || CFG_getShowFolderNamesAtRoot();
+ // (the "All games" root lists games, not folders, so names always show)
+ list_show_entry_names = stack->count > 1 || CFG_getShowFolderNamesAtRoot() || all_games_root;
// load folder background
char defaultBgPath[512];
@@ -3027,8 +3642,17 @@ int main (int argc, char *argv[]) {
// Safeguard: If no background is available, still render the text to leave the user a way out
list_show_entry_names = true;
}
+ // grid tiles show their own art, and the selection pill/scroll text layers are list-only
+ int grid_mode = Grid_active();
+ int grid_show_names = list_show_entry_names;
+ if (grid_mode) {
+ list_show_entry_names = false;
+ had_thumb = 0;
+ ox = screen->w;
+ }
+
// load game thumbnails
- if (total > 0) {
+ if (total > 0 && !grid_mode) {
if(CFG_getShowGameArt()) {
char thumbpath[1024];
snprintf(thumbpath, sizeof(thumbpath), "%s/.media/%s.png", rompath, res_copy);
@@ -3048,8 +3672,11 @@ int main (int argc, char *argv[]) {
}
// buttons
+ // with "Resume with A" and something to resume, A and X trade labels
+ int resume_with_a = can_resume && CFG_getResumeWithA();
+ char* a_label = resume_with_a ? "RESUME" : "OPEN";
if (show_setting && !GetHDMI()) GFX_blitHardwareHints(screen, show_setting);
- else if (can_resume) GFX_blitButtonGroup((char*[]){ "X","RESUME", NULL }, 0, screen, 0);
+ else if (can_resume) GFX_blitButtonGroup((char*[]){ "X", resume_with_a ? "OPEN" : "RESUME", NULL }, 0, screen, 0);
else GFX_blitButtonGroup((char*[]){
BTN_SLEEP==BTN_POWER?"POWER":"MENU",
BTN_SLEEP==BTN_POWER||simple_mode?"SLEEP":"INFO",
@@ -3062,10 +3689,10 @@ int main (int argc, char *argv[]) {
}
else {
if (stack->count>1) {
- GFX_blitButtonGroup((char*[]){ "B","BACK", "A","OPEN", NULL }, 1, screen, 1);
+ GFX_blitButtonGroup((char*[]){ "B","BACK", "A",a_label, NULL }, 1, screen, 1);
}
else {
- GFX_blitButtonGroup((char*[]){ "A","OPEN", NULL }, 0, screen, 1);
+ GFX_blitButtonGroup((char*[]){ "A",a_label, NULL }, 0, screen, 1);
}
}
@@ -3075,7 +3702,11 @@ int main (int argc, char *argv[]) {
previousY = previous_row * PILL_SIZE;
targetY = selected_row * PILL_SIZE;
SDL_Color text_color = uintToColour(THEME_COLOR4_255); // list text color
- for (int i = top->start, j = 0; i < top->end; i++, j++) {
+ if (grid_mode) {
+ Grid_render(top, grid_show_names, ow);
+ is_scrolling = 0;
+ }
+ for (int i = top->start, j = 0; !grid_mode && i < top->end; i++, j++) {
Entry* entry = top->entries->items[i];
char* entry_name = entry->name;
char* entry_unique = entry->unique;
@@ -3083,8 +3714,9 @@ int main (int argc, char *argv[]) {
bool row_is_selected = (j == selected_row);
bool row_is_top = (i == top->start);
bool row_has_moved = (previous_row != selected_row || previous_depth != stack->count);
+ // a larger screen margin moves the status pill further in, keep clear of it
if (row_is_top && !(had_thumb))
- available_width -= ow;
+ available_width -= ow + MAX(0, GFX_getScreenMargin() - SCALE1(PADDING));
trimSortingMeta(&entry_name);
if (entry_unique) // Only render if a unique name exists
@@ -3338,7 +3970,8 @@ int main (int argc, char *argv[]) {
}
int available_width = (had_thumb ? ox + SCALE1(BUTTON_MARGIN) : screen->w - SCALE1(BUTTON_MARGIN)) - SCALE1(PADDING * 2);
- if (top->selected == top->start && !had_thumb) available_width -= ow;
+ if (top->selected == top->start && !had_thumb)
+ available_width -= ow + MAX(0, GFX_getScreenMargin() - SCALE1(PADDING));
SDL_Color text_color = uintToColour(THEME_COLOR5_255);
@@ -3430,6 +4063,7 @@ int main (int argc, char *argv[]) {
if(blackBG) SDL_FreeSurface(blackBG);
if (folderbgbmp) SDL_FreeSurface(folderbgbmp);
if (thumbbmp) SDL_FreeSurface(thumbbmp);
+ Grid_freeArt();
QuitSettings();
}
diff --git a/workspace/all/settings/menu.cpp b/workspace/all/settings/menu.cpp
index b17a0cabb9..ec9f6d6e92 100644
--- a/workspace/all/settings/menu.cpp
+++ b/workspace/all/settings/menu.cpp
@@ -312,9 +312,71 @@ MenuList::MenuList(MenuItemType type, const std::string &descp, std::vector predicate)
+{
+ {
+ WriteLock w(itemLock);
+ if (all_items.empty())
+ all_items = items;
+ visible_if[item] = predicate;
+ }
+ refreshVisibility();
+}
+
+bool MenuList::refreshVisibility()
+{
+ if (visible_if.empty())
+ return false;
+
+ WriteLock w(itemLock);
+
+ std::vector visible;
+ for (auto itm : all_items)
+ {
+ auto it = visible_if.find(itm);
+ if (it == visible_if.end() || !it->second || it->second())
+ visible.push_back(itm);
+ }
+ if (visible == items)
+ return false;
+
+ // keep the same item selected, or the same row if it was the one hidden
+ AbstractMenuItem *selected = (scope.selected >= 0 && scope.selected < (int)items.size()) ? items[scope.selected] : nullptr;
+ int previous_index = scope.selected;
+
+ // items that just appeared may hold a stale value (eg. after a reset while hidden)
+ for (auto itm : visible)
+ if (std::find(items.begin(), items.end(), itm) == items.end())
+ itm->initSelection();
+
+ items = visible;
+
+ int count = (int)items.size();
+ int index = (int)(std::find(items.begin(), items.end(), selected) - items.begin());
+ if (index >= count)
+ index = std::min(std::max(previous_index, 0), std::max(count - 1, 0));
+
+ int rows = std::min(count, scope.max_visible_options);
+ int start = scope.start;
+ if (index < start)
+ start = index;
+ if (index >= start + rows)
+ start = index - rows + 1;
+ start = std::max(0, std::min(start, count - rows));
+
+ scope.count = count;
+ scope.visible_rows = rows;
+ scope.selected = index;
+ scope.start = start;
+ scope.end = start + rows;
+ return true;
}
void MenuList::performLayout(const SDL_Rect &dst)
@@ -418,6 +480,10 @@ bool MenuList::selectByName(const std::string &name)
// returns true if the input was handled
InputReactionHint MenuList::handleInput(int &dirty, int &quit)
{
+ // pick up option changes from the previous frame that show/hide other items
+ if (refreshVisibility())
+ dirty = 1;
+
bool tryDismiss = false;
{
ReadLock r(overlayLock);
@@ -892,7 +958,8 @@ void MenuList::drawMainItem(SDL_Surface *surface, const SDL_Rect &dst, const Abs
void MenuList::resetAllItems()
{
ReadLock r(itemLock);
- for(auto item : items) {
+ // reset hidden items too, they are part of the menu even when not shown
+ for(auto item : all_items.empty() ? items : all_items) {
if(item->on_reset) {
item->on_reset();
item->initSelection();
diff --git a/workspace/all/settings/menu.hpp b/workspace/all/settings/menu.hpp
index 2e3b981817..10682087eb 100644
--- a/workspace/all/settings/menu.hpp
+++ b/workspace/all/settings/menu.hpp
@@ -14,6 +14,7 @@ extern "C"
#include
#include
#include
+#include