diff --git a/skeleton/BASE/Palettes/README.txt b/skeleton/BASE/Palettes/README.txt new file mode 100644 index 000000000..bffad1e26 --- /dev/null +++ b/skeleton/BASE/Palettes/README.txt @@ -0,0 +1,39 @@ +NextUI Color Palettes +---------------------------------------- + +Drop custom color palette files (.txt) in this folder. They will show up under +Settings > Appearance > Color Palette, alongside the built-in palettes, and can be +shared with others by simply copying the file. + +Each palette is a plain text file. Example (my_palette.txt): + + version=1 + name=Example + color1=0xffffffff + color2=0x9b2257ff + color3=0x1e2329ff + color4=0xffffffff + color5=0x000000ff + color6=0x000000ff + color7=0xffffffff + +Fields +------ +version Palette file format version. Use 1. Files with a version newer than the + running NextUI build supports are ignored. +name The label shown in the menu. If omitted, the file name is used (with + underscores turned into spaces). +color1 Main color - main UI elements. +color2 Primary accent - highlights important things. +color3 Secondary accent. +color4 List text. +color5 List text (selected). +color6 Hint / info text (button hints). +color7 Background - used when no background image is set. + +Colors are packed hex. Both 0xRRGGBB and 0xRRGGBBAA are accepted; when the alpha +byte is omitted the color is treated as fully opaque. Any color you leave out +falls back to the NextUI default for that slot. + +Built-in palettes live with the system files and cannot be edited. Copy one here +and rename it to use it as a starting point for your own. diff --git a/skeleton/SYSTEM/res/palettes/Brick Blush.txt b/skeleton/SYSTEM/res/palettes/Brick Blush.txt new file mode 100644 index 000000000..56545b78f --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Brick Blush.txt @@ -0,0 +1,9 @@ +version=1 +name=Brick Blush +color1=0xB5442EFF +color2=0xF6DFD9FF +color3=0xFBEAE6FF +color4=0x33201DFF +color5=0xFBEAE6FF +color6=0x8A6259FF +color7=0xFBECE8FF diff --git a/skeleton/SYSTEM/res/palettes/Catppuccin Frappe.txt b/skeleton/SYSTEM/res/palettes/Catppuccin Frappe.txt new file mode 100644 index 000000000..f5791414c --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Catppuccin Frappe.txt @@ -0,0 +1,9 @@ +version=1 +name=Catppuccin Frappe +color1=0xA6D189FF +color2=0x292C3CFF +color3=0x232634FF +color4=0xC6D0F5FF +color5=0x232634FF +color6=0xA5ADCEFF +color7=0x303446FF diff --git a/skeleton/SYSTEM/res/palettes/Catppuccin Latte.txt b/skeleton/SYSTEM/res/palettes/Catppuccin Latte.txt new file mode 100644 index 000000000..0892d3d79 --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Catppuccin Latte.txt @@ -0,0 +1,9 @@ +version=1 +name=Catppuccin Latte +color1=0x8839EFFF +color2=0xE6E9EFFF +color3=0xEFF1F5FF +color4=0x4C4F69FF +color5=0xEFF1F5FF +color6=0x6C6F85FF +color7=0xEFF1F5FF diff --git a/skeleton/SYSTEM/res/palettes/Catppuccin Macchiato.txt b/skeleton/SYSTEM/res/palettes/Catppuccin Macchiato.txt new file mode 100644 index 000000000..8ec80093d --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Catppuccin Macchiato.txt @@ -0,0 +1,9 @@ +version=1 +name=Catppuccin Macchiato +color1=0xF5A97FFF +color2=0x1E2030FF +color3=0x24273AFF +color4=0xCAD3F5FF +color5=0x24273AFF +color6=0xA5ADCBFF +color7=0x24273AFF diff --git a/skeleton/SYSTEM/res/palettes/Catppuccin Mocha.txt b/skeleton/SYSTEM/res/palettes/Catppuccin Mocha.txt new file mode 100644 index 000000000..1066c27f3 --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Catppuccin Mocha.txt @@ -0,0 +1,9 @@ +version=1 +name=Catppuccin Mocha +color1=0xCBA6F7FF +color2=0x181825FF +color3=0x1E1E2EFF +color4=0xCDD6F4FF +color5=0x1E1E2EFF +color6=0xA6ADC8FF +color7=0x1E1E2EFF diff --git a/skeleton/SYSTEM/res/palettes/Charcoal Coral.txt b/skeleton/SYSTEM/res/palettes/Charcoal Coral.txt new file mode 100644 index 000000000..60c76023d --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Charcoal Coral.txt @@ -0,0 +1,9 @@ +version=1 +name=Charcoal Coral +color1=0xFF6B5BFF +color2=0x161416FF +color3=0x2A0D06FF +color4=0xF2F0ECFF +color5=0x2A0D06FF +color6=0x948F8CFF +color7=0x1D1B1EFF diff --git a/skeleton/SYSTEM/res/palettes/Deep Violet.txt b/skeleton/SYSTEM/res/palettes/Deep Violet.txt new file mode 100644 index 000000000..e0400abe7 --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Deep Violet.txt @@ -0,0 +1,9 @@ +version=1 +name=Deep Violet +color1=0x6C4BC9FF +color2=0xE7DCF7FF +color3=0xF4EFFDFF +color4=0x241B3DFF +color5=0xF4EFFDFF +color6=0x6E6389FF +color7=0xF1EAFAFF diff --git a/skeleton/SYSTEM/res/palettes/Default.txt b/skeleton/SYSTEM/res/palettes/Default.txt new file mode 100644 index 000000000..15416d8d9 --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Default.txt @@ -0,0 +1,9 @@ +version=1 +name=Default +color1=0xffffffff +color2=0x9b2257ff +color3=0x1e2329ff +color4=0xffffffff +color5=0x000000ff +color6=0xffffffff +color7=0x000000ff diff --git a/skeleton/SYSTEM/res/palettes/Forest Lime.txt b/skeleton/SYSTEM/res/palettes/Forest Lime.txt new file mode 100644 index 000000000..6bda64080 --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Forest Lime.txt @@ -0,0 +1,9 @@ +version=1 +name=Forest Lime +color1=0xB7DD5BFF +color2=0x0A1712FF +color3=0x1B2708FF +color4=0xE7EFE7FF +color5=0x1B2708FF +color6=0x7F998AFF +color7=0x0F1F18FF diff --git a/skeleton/SYSTEM/res/palettes/Ink - Gold.txt b/skeleton/SYSTEM/res/palettes/Ink - Gold.txt new file mode 100644 index 000000000..ae874707d --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Ink - Gold.txt @@ -0,0 +1,9 @@ +version=1 +name=Ink & Gold +color1=0xF2A93BFF +color2=0x0C0E17FF +color3=0x241A05FF +color4=0xE7E6F2FF +color5=0x241A05FF +color6=0x8A87A3FF +color7=0x12141FFF diff --git a/skeleton/SYSTEM/res/palettes/Maroon Rose.txt b/skeleton/SYSTEM/res/palettes/Maroon Rose.txt new file mode 100644 index 000000000..c71c752b9 --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Maroon Rose.txt @@ -0,0 +1,9 @@ +version=1 +name=Maroon Rose +color1=0xE9A6A0FF +color2=0x1C0B0EFF +color3=0x2E100CFF +color4=0xF3E6E4FF +color5=0x2E100CFF +color6=0x9C7B78FF +color7=0x271014FF diff --git a/skeleton/SYSTEM/res/palettes/MinUI.txt b/skeleton/SYSTEM/res/palettes/MinUI.txt new file mode 100644 index 000000000..5ac2d164b --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/MinUI.txt @@ -0,0 +1,9 @@ +version=1 +name=MinUI +color1=0xffffffff +color2=0x262626ff +color3=0x999999ff +color4=0xffffffff +color5=0x000000ff +color6=0xffffffff +color7=0x000000ff \ No newline at end of file diff --git a/skeleton/SYSTEM/res/palettes/Mossy Sage.txt b/skeleton/SYSTEM/res/palettes/Mossy Sage.txt new file mode 100644 index 000000000..26f2537fa --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Mossy Sage.txt @@ -0,0 +1,9 @@ +version=1 +name=Mossy Sage +color1=0x4B6B3FFF +color2=0xE4EBD9FF +color3=0xEBF3E4FF +color4=0x1F2A1BFF +color5=0xEBF3E4FF +color6=0x647459FF +color7=0xEEF2E6FF diff --git a/skeleton/SYSTEM/res/palettes/Mustard Butter.txt b/skeleton/SYSTEM/res/palettes/Mustard Butter.txt new file mode 100644 index 000000000..b3432f6eb --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Mustard Butter.txt @@ -0,0 +1,9 @@ +version=1 +name=Mustard Butter +color1=0xB08117FF +color2=0xF5E9C4FF +color3=0xFDF3DAFF +color4=0x2E2610FF +color5=0xFDF3DAFF +color6=0x8A7A45FF +color7=0xFBF3DCFF diff --git a/skeleton/SYSTEM/res/palettes/Plum Magenta.txt b/skeleton/SYSTEM/res/palettes/Plum Magenta.txt new file mode 100644 index 000000000..c2d5023c7 --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Plum Magenta.txt @@ -0,0 +1,9 @@ +version=1 +name=Plum Magenta +color1=0xD6559EFF +color2=0x170F1DFF +color3=0x2E0A1FFF +color4=0xEEE6F2FF +color5=0x2E0A1FFF +color6=0x93849EFF +color7=0x1F1526FF diff --git a/skeleton/SYSTEM/res/palettes/Slate Cyan.txt b/skeleton/SYSTEM/res/palettes/Slate Cyan.txt new file mode 100644 index 000000000..4a49df984 --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Slate Cyan.txt @@ -0,0 +1,9 @@ +version=1 +name=Slate Cyan +color1=0x45CFC3FF +color2=0x111A28FF +color3=0x0A2320FF +color4=0xE6EDF3FF +color5=0x0A2320FF +color6=0x7E8FA3FF +color7=0x182233FF diff --git a/skeleton/SYSTEM/res/palettes/Teal Powder.txt b/skeleton/SYSTEM/res/palettes/Teal Powder.txt new file mode 100644 index 000000000..e8f6453c5 --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Teal Powder.txt @@ -0,0 +1,9 @@ +version=1 +name=Teal Powder +color1=0x1E6E76FF +color2=0xDCEBEFFF +color3=0xE7F5F5FF +color4=0x17232EFF +color5=0xE7F5F5FF +color6=0x5B7480FF +color7=0xE9F2F5FF diff --git a/skeleton/SYSTEM/res/palettes/Terracotta Cream.txt b/skeleton/SYSTEM/res/palettes/Terracotta Cream.txt new file mode 100644 index 000000000..3463e7060 --- /dev/null +++ b/skeleton/SYSTEM/res/palettes/Terracotta Cream.txt @@ -0,0 +1,9 @@ +version=1 +name=Terracotta Cream +color1=0xC1602EFF +color2=0xF1E8D9FF +color3=0xFCEEE4FF +color4=0x2B2118FF +color5=0xFCEEE4FF +color6=0x7A6E5CFF +color7=0xF7F1E7FF diff --git a/workspace/all/common/api.c b/workspace/all/common/api.c index aa79c766f..f70c2bc31 100644 --- a/workspace/all/common/api.c +++ b/workspace/all/common/api.c @@ -3327,7 +3327,7 @@ FALLBACK_IMPLEMENTATION void PLAT_pollInput(void) { uint8_t code = event.key.keysym.scancode; pressed = event.type == SDL_KEYDOWN; - LOG_info("key event: %i (%i)\n", code,pressed); + // LOG_info("key event: %i (%i)\n", code,pressed); if (code == CODE_UP) { btn = BTN_DPAD_UP; @@ -3453,7 +3453,7 @@ FALLBACK_IMPLEMENTATION void PLAT_pollInput(void) { uint8_t joy = event.jbutton.button; pressed = event.type == SDL_JOYBUTTONDOWN; - LOG_info("joy event: %i (%i)\n", joy,pressed); + // LOG_info("joy event: %i (%i)\n", joy,pressed); if (joy == JOY_UP) { btn = BTN_DPAD_UP; @@ -3579,7 +3579,7 @@ FALLBACK_IMPLEMENTATION void PLAT_pollInput(void) { int hats[4] = {-1, -1, -1, -1}; // -1=no change,0=up,1=down,2=left,3=right btn_ids int hat = event.jhat.value; - LOG_info("hat event: %i\n", hat); + // LOG_info("hat event: %i\n", hat); // TODO: safe to assume hats will always be the primary dpad? // TODO: this is literally a bitmask, make it one (oh, except there's 3 states...) switch (hat) diff --git a/workspace/all/common/config.c b/workspace/all/common/config.c index 3bc4ba8e8..cc735eec7 100644 --- a/workspace/all/common/config.c +++ b/workspace/all/common/config.c @@ -44,6 +44,10 @@ void CFG_defaults(NextUISettings *cfg) .gameArtWidth = CFG_DEFAULT_GAMEARTWIDTH, .showFolderNamesAtRoot = CFG_DEFAULT_SHOWFOLDERNAMESATROOT, .inputPromptStyle = CFG_DEFAULT_INPUT_PROMPT_STYLE, + .paletteName = CFG_DEFAULT_PALETTE_NAME, + .customColors = {CFG_DEFAULT_COLOR1, CFG_DEFAULT_COLOR2, CFG_DEFAULT_COLOR3, + CFG_DEFAULT_COLOR4, CFG_DEFAULT_COLOR5, CFG_DEFAULT_COLOR6, + CFG_DEFAULT_COLOR7}, .showClock = CFG_DEFAULT_SHOWCLOCK, .clock24h = CFG_DEFAULT_CLOCK24H, @@ -124,6 +128,14 @@ static inline uint32_t parseHexColor(const char *hexColor) { return value; } +uint32_t CFG_parseHexColor(const char *hexColor) +{ + return parseHexColor(hexColor); +} + +// Defined below, alongside the rest of the palette-name accessors. +static void setPaletteNameRaw(const char *name); + void CFG_init(FontLoad_callback_t cb, ColorSet_callback_t ccb) { CFG_defaults(&settings); @@ -159,6 +171,17 @@ void CFG_init(FontLoad_callback_t cb, ColorSet_callback_t ccb) fontLoaded = true; continue; } + if (strncmp(line, "palette=", 8) == 0) + { + char *value = line + 8; + value[strcspn(value, "\r\n")] = 0; + // Restore the persisted name as-is; the colors it names are loaded + // independently a few lines below in this same pass, so the pair is + // already consistent. Deliberately bypasses CFG_applyPalette, which + // would overwrite those colors with a (possibly stale) palette's. + setPaletteNameRaw(value); + continue; + } if(strncmp(line, "color1=", 7) == 0) { char *value = line + 7; @@ -577,6 +600,65 @@ void CFG_setColor(int color_id, uint32_t color) settings.onColorSet(); } +// Predefined color palette selection (see palette.c for enumeration/loading) +// +// A non-empty palette name and the 7 colors it points at must never diverge, so +// setting a non-empty name is only possible atomically with the colors it names +// (CFG_applyPalette). setPaletteNameRaw is a private escape hatch used solely to +// restore already-consistent state read back from minuisettings.txt, where the +// colors are loaded independently in the same pass. + +static void setPaletteNameRaw(const char *name) +{ + if (!name) + name = ""; + strncpy(settings.paletteName, name, sizeof(settings.paletteName) - 1); + settings.paletteName[sizeof(settings.paletteName) - 1] = '\0'; +} + +const char* CFG_getPaletteName(void) +{ + return settings.paletteName; +} + +void CFG_applyPalette(const char *name, const uint32_t colors[7]) +{ + // Snapshot the current colors before they're overwritten, but only when + // actually leaving Custom - switching directly between two named palettes must + // not clobber the one saved "last custom" backup with an intermediate palette's + // colors. + if (settings.paletteName[0] == '\0') + { + settings.customColors[0] = settings.color1_255; + settings.customColors[1] = settings.color2_255; + settings.customColors[2] = settings.color3_255; + settings.customColors[3] = settings.color4_255; + settings.customColors[4] = settings.color5_255; + settings.customColors[5] = settings.color6_255; + settings.customColors[6] = settings.color7_255; + } + + for (int i = 0; i < 7; i++) + CFG_setColor(i + 1, colors[i]); + setPaletteNameRaw(name); +} + +void CFG_clearPalette(void) +{ + setPaletteNameRaw(""); +} + +void CFG_selectCustomPalette(void) +{ + // Only restore when actually transitioning away from a palette: if we're + // already Custom, the current colors are whatever the user has been editing + // and must not be clobbered by the (now stale) backup. + if (settings.paletteName[0] != '\0') + for (int i = 0; i < 7; i++) + CFG_setColor(i + 1, settings.customColors[i]); + setPaletteNameRaw(""); +} + bool CFG_getShowFolderNamesAtRoot(void) { return settings.showFolderNamesAtRoot; @@ -1185,6 +1267,10 @@ void CFG_get(const char *key, char *value) else sprintf(value, "1"); } + else if (strcmp(key, "palette") == 0) + { + sprintf(value, "\"%s\"", CFG_getPaletteName()); + } else if (strcmp(key, "color1") == 0) { sprintf(value, "\"0x%08X\"", CFG_getColor(COLOR_MAIN)); @@ -1444,6 +1530,7 @@ void CFG_sync(void) fprintf(file, "font=0\n"); else fprintf(file, "font=%s\n", settings.fontFile); + fprintf(file, "palette=%s\n", settings.paletteName); fprintf(file, "color1=0x%08X\n", settings.color1_255); fprintf(file, "color2=0x%08X\n", settings.color2_255); fprintf(file, "color3=0x%08X\n", settings.color3_255); @@ -1513,6 +1600,7 @@ void CFG_print(void) printf("\t\"font\": 0,\n"); else printf("\t\"font\": 1,\n"); + printf("\t\"palette\": \"%s\",\n", settings.paletteName); printf("\t\"color1\": \"0x%08X\",\n", settings.color1_255); printf("\t\"color2\": \"0x%08X\",\n", settings.color2_255); printf("\t\"color3\": \"0x%08X\",\n", settings.color3_255); diff --git a/workspace/all/common/config.h b/workspace/all/common/config.h index 5d23e3b27..3d8b7e80c 100644 --- a/workspace/all/common/config.h +++ b/workspace/all/common/config.h @@ -107,6 +107,9 @@ typedef struct int gameSwitcherCurtain; double gameArtWidth; // [0,1] -> 0-100% of screen width int inputPromptStyle; // enum + char paletteName[64]; // selected predefined color palette; empty == custom colors. keep in sync with PALETTE_NAME_MAX in palette.h + uint32_t customColors[7]; // last custom (non-palette) colors, restored by CFG_selectCustomPalette. + // in-memory only (not persisted): resets to defaults each app launch // font loading/unloading callback FontLoad_callback_t onFontChange; @@ -197,6 +200,7 @@ typedef struct #define CFG_DEFAULT_COLOR_LIST_TEXT_SELECTED CFG_DEFAULT_COLOR5 #define CFG_DEFAULT_COLOR_HINT CFG_DEFAULT_COLOR6 #define CFG_DEFAULT_COLOR_BACKGROUND CFG_DEFAULT_COLOR7 +#define CFG_DEFAULT_PALETTE_NAME "" // empty == custom colors #define CFG_DEFAULT_THUMBRADIUS 20 // unscaled! #define CFG_DEFAULT_SHOWCLOCK false #define CFG_DEFAULT_CLOCK24H true @@ -276,6 +280,27 @@ void CFG_setFontStyle(int style); // 3 - Background Color (unused) uint32_t CFG_getColor(int id); void CFG_setColor(int id, uint32_t color); +// Parse a hex color string into a packed 0xRRGGBBAA value. Accepts both legacy +// RGB (RRGGBB, treated as opaque) and RGBA (RRGGBBAA), with or without "0x". +uint32_t CFG_parseHexColor(const char *hexColor); +// The name of the currently selected predefined color palette (see palette.h), or +// "" for custom colors. +const char* CFG_getPaletteName(void); +// Atomically set all 7 UI colors and record `name` as the selected palette, so the +// persisted palette name can never point at a color set that doesn't match it. +// This is the only way to select a named (non-custom) palette; there is no setter +// for the name alone. If a palette isn't already active, the current colors are +// preserved first so CFG_selectCustomPalette() can restore them later. +void CFG_applyPalette(const char *name, const uint32_t colors[7]); +// Detach from any predefined palette without changing the current colors ("Custom"). +// Used when an individual color edit invalidates the current palette selection - +// deliberately does not restore the pre-palette colors, since the edit itself is +// the new intended state. For that, see CFG_selectCustomPalette(). +void CFG_clearPalette(void); +// Explicitly select "Custom": if a predefined palette is active, restores the +// colors that were in effect before it was applied and clears the palette name. +// A no-op if already on Custom (colors are left untouched, not reset). +void CFG_selectCustomPalette(void); // Time in secs before the device enters screen-off mode. uint32_t CFG_getScreenTimeoutSecs(void); void CFG_setScreenTimeoutSecs(uint32_t secs); diff --git a/workspace/all/common/palette.c b/workspace/all/common/palette.c new file mode 100644 index 000000000..4a3c17f15 --- /dev/null +++ b/workspace/all/common/palette.c @@ -0,0 +1,133 @@ +#include "palette.h" + +#include +#include +#include +#include +#include + +#include "defines.h" +#include "config.h" + +static const uint32_t palette_default_colors[PALETTE_COLOR_COUNT] = { + CFG_DEFAULT_COLOR1, CFG_DEFAULT_COLOR2, CFG_DEFAULT_COLOR3, + CFG_DEFAULT_COLOR4, CFG_DEFAULT_COLOR5, CFG_DEFAULT_COLOR6, + CFG_DEFAULT_COLOR7, +}; + +// Derive a display name from a filename: strip .txt, replace underscores with spaces. +static void palette_nameFromFilename(const char *filename, char *out, size_t out_size) +{ + size_t len = strlen(filename); + if (len > 4 && strcasecmp(filename + len - 4, ".txt") == 0) + len -= 4; + if (len >= out_size) + len = out_size - 1; + for (size_t i = 0; i < len; i++) + out[i] = (filename[i] == '_') ? ' ' : filename[i]; + out[len] = '\0'; +} + +// Parse a single palette file. Returns true on success and a supported version. +static bool palette_loadFile(const char *path, const char *filename, bool builtin, ColorPalette *out) +{ + FILE *file = fopen(path, "r"); + if (!file) + return false; + + memset(out, 0, sizeof(*out)); + out->version = 1; // assumed when version= is omitted + out->builtin = builtin; + strncpy(out->path, path, sizeof(out->path) - 1); + for (int i = 0; i < PALETTE_COLOR_COUNT; i++) + out->colors[i] = palette_default_colors[i]; + + bool haveName = false; + char line[256]; + while (fgets(line, sizeof(line), file)) + { + int temp_value; + if (sscanf(line, "version=%i", &temp_value) == 1) + { + out->version = temp_value; + continue; + } + if (strncmp(line, "name=", 5) == 0) + { + char *value = line + 5; + value[strcspn(value, "\r\n")] = 0; + strncpy(out->name, value, sizeof(out->name) - 1); + haveName = out->name[0] != '\0'; + continue; + } + for (int i = 0; i < PALETTE_COLOR_COUNT; i++) + { + char key[8]; + snprintf(key, sizeof(key), "color%d=", i + 1); + size_t keylen = strlen(key); + if (strncmp(line, key, keylen) == 0) + { + char *value = line + keylen; + value[strcspn(value, "\r\n")] = 0; + out->colors[i] = CFG_parseHexColor(value); + break; + } + } + } + fclose(file); + + // Skip palettes authored for a newer format than we understand. + if (out->version > PALETTE_VERSION_MAX) + return false; + + if (!haveName) + palette_nameFromFilename(filename, out->name, sizeof(out->name)); + + return true; +} + +static int palette_scanDir(const char *dirpath, bool builtin, ColorPalette *out, int max, int count) +{ + DIR *dir = opendir(dirpath); + if (!dir) + return count; + + struct dirent *ent; + while ((ent = readdir(dir)) != NULL && count < max) + { + if (ent->d_name[0] == '.') + continue; + size_t len = strlen(ent->d_name); + if (len < 5 || strcasecmp(ent->d_name + len - 4, ".txt") != 0) + continue; + + char path[PALETTE_PATH_MAX]; + snprintf(path, sizeof(path), "%s/%s", dirpath, ent->d_name); + if (palette_loadFile(path, ent->d_name, builtin, &out[count])) + count++; + } + closedir(dir); + return count; +} + +int PALETTE_enumerate(ColorPalette *out, int max) +{ + if (!out || max <= 0) + return 0; + + int count = 0; + // Built-ins first (read-only), then user/community drop-ins. + count = palette_scanDir(RES_PATH "/palettes", true, out, max, count); + count = palette_scanDir(SDCARD_PATH "/Palettes", false, out, max, count); + return count; +} + +void PALETTE_apply(const ColorPalette *palette) +{ + if (!palette) + return; + // CFG_applyPalette sets the 7 colors and the palette name as one atomic step, + // so the persisted "current palette" can never point at a color set that + // doesn't match it. + CFG_applyPalette(palette->name, palette->colors); +} diff --git a/workspace/all/common/palette.h b/workspace/all/common/palette.h new file mode 100644 index 000000000..bb704dd46 --- /dev/null +++ b/workspace/all/common/palette.h @@ -0,0 +1,40 @@ +#ifndef __PALETTE_H__ +#define __PALETTE_H__ + +#include +#include + +// Predefined color palettes: file-based color presets users can pick from in +// Settings, in addition to the fully custom color1-7 controls. "Palette" is +// deliberately scoped to colors only - a future "Theme" feature may bundle +// palettes with other assets (fonts, backgrounds, etc.). See config.h for the +// persisted "currently selected palette" state (CFG_getPaletteName/ +// CFG_applyPalette/CFG_clearPalette). + +// Highest palette-file format version this build understands. Palette files +// with a higher version= are skipped during enumeration. +#define PALETTE_VERSION_MAX 1 +#define PALETTE_NAME_MAX 64 // keep in sync with NextUISettings.paletteName in config.h +#define PALETTE_PATH_MAX 512 +#define PALETTE_COLOR_COUNT 7 + +typedef struct +{ + int version; // palette-file format version + char name[PALETTE_NAME_MAX]; // display label + char path[PALETTE_PATH_MAX]; // absolute path to the palette file + bool builtin; // true if shipped in RES_PATH/palettes (read-only) + uint32_t colors[PALETTE_COLOR_COUNT]; // packed RGBA, index 0 == color1 +} ColorPalette; + +// Enumerate predefined color palettes found on disk. +// Scans RES_PATH/palettes (built-ins, listed first) and SDCARD_PATH/Palettes (user). +// Files whose version= exceeds PALETTE_VERSION_MAX are skipped. Missing colors fall +// back to the CFG defaults. Writes up to max entries into out; returns the count. +int PALETTE_enumerate(ColorPalette *out, int max); + +// Apply a palette's colors to the current settings and record it as the selected +// palette. Fires the color-set callback per color; does not modify the palette file. +void PALETTE_apply(const ColorPalette *palette); + +#endif diff --git a/workspace/all/minput/minput.c b/workspace/all/minput/minput.c index c4abdf611..5656a8111 100644 --- a/workspace/all/minput/minput.c +++ b/workspace/all/minput/minput.c @@ -18,7 +18,7 @@ static int getButtonWidth(char* label) { if (strlen(label)<=2) w = SCALE1(BUTTON_SIZE); else { - text = TTF_RenderUTF8_Blended(font.tiny, label, COLOR_BUTTON_TEXT); + text = TTF_RenderUTF8_Blended(font.tiny, label, ALT_BUTTON_TEXT_COLOR); w = SCALE1(BUTTON_SIZE) + text->w; SDL_FreeSurface(text); } @@ -28,17 +28,22 @@ static int getButtonWidth(char* label) { static void blitButton(char* label, SDL_Surface* dst, int pressed, int x, int y, int w) { SDL_Rect point = {x,y}; SDL_Surface* text; - + + // Pressed reads as a lit button (main fill + accent2 label, matching GFX_blitButton), + // released reads as a hole punched through the tray to the background. + uint32_t fill = pressed ? THEME_COLOR1 : THEME_COLOR7; + SDL_Color text_color = pressed ? ALT_BUTTON_TEXT_COLOR : uintToColour(THEME_COLOR6_255); + int len = strlen(label); if (len<=2) { - text = TTF_RenderUTF8_Blended(len==2?font.small:font.medium, label, COLOR_BUTTON_TEXT); - GFX_blitAsset(pressed?ASSET_BUTTON:ASSET_HOLE, NULL, dst, &point); + text = TTF_RenderUTF8_Blended(len==2?font.small:font.medium, label, text_color); + GFX_blitAssetColor(ASSET_BUTTON, NULL, dst, &point, fill); SDL_BlitSurface(text, NULL, dst, &(SDL_Rect){point.x+(SCALE1(BUTTON_SIZE)-text->w)/2,point.y+(SCALE1(BUTTON_SIZE)-text->h)/2}); } else { - text = TTF_RenderUTF8_Blended(font.tiny, label, COLOR_BUTTON_TEXT); + text = TTF_RenderUTF8_Blended(font.tiny, label, text_color); w = w ? w : SCALE1(BUTTON_SIZE)/2+text->w; - GFX_blitPill(pressed?ASSET_BUTTON:ASSET_HOLE, dst, &(SDL_Rect){point.x,point.y,w,SCALE1(BUTTON_SIZE)}); + GFX_blitPillColor(ASSET_BUTTON, dst, &(SDL_Rect){point.x,point.y,w,SCALE1(BUTTON_SIZE)}, fill, RGB_WHITE); SDL_BlitSurface(text, NULL, dst, &(SDL_Rect){point.x+(w-text->w)/2,point.y+(SCALE1(BUTTON_SIZE)-text->h)/2,text->w,text->h}); } @@ -118,7 +123,7 @@ int main(int argc , char* argv[]) { if (has_L2) w += getButtonWidth("L2") + SCALE1(BUTTON_MARGIN); if (has_L4) w += getButtonWidth("L4") + SCALE1(BUTTON_MARGIN); if (!has_L2 && !has_L4) x += SCALE1(PILL_SIZE); - GFX_blitPillColor(ASSET_WHITE_PILL, screen, &(SDL_Rect){x, y, w}, THEME_COLOR3, RGB_WHITE); + GFX_blitPillLight(ASSET_WHITE_PILL, screen, &(SDL_Rect){x, y, w}); blitButton("L1", screen, PAD_isPressed(BTN_L1), x + SCALE1(BUTTON_MARGIN), y+SCALE1(BUTTON_MARGIN),0); if (has_L2) { @@ -147,7 +152,7 @@ int main(int argc , char* argv[]) { x = FIXED_WIDTH - w - SCALE1(BUTTON_MARGIN + PADDING); if (!has_R2 && !has_R4) x -= SCALE1(PILL_SIZE); - GFX_blitPillColor(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,w}, THEME_COLOR3, RGB_WHITE); + GFX_blitPillLight(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,w}); if(has_R4) { x+= SCALE1(BUTTON_MARGIN); @@ -168,24 +173,24 @@ int main(int argc , char* argv[]) { int y = oy + SCALE1(PILL_SIZE*2); int o = SCALE1(BUTTON_MARGIN); - SDL_FillRect(screen, &(SDL_Rect){x,y+SCALE1(PILL_SIZE/2),SCALE1(PILL_SIZE),SCALE1(PILL_SIZE*2)}, THEME_COLOR3); - GFX_blitPillColor(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,0}, THEME_COLOR3, RGB_WHITE); + SDL_FillRect(screen, &(SDL_Rect){x,y+SCALE1(PILL_SIZE/2),SCALE1(PILL_SIZE),SCALE1(PILL_SIZE*2)}, THEME_COLOR2); + GFX_blitPillLight(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,0}); blitButton("U", screen, PAD_isPressed(BTN_DPAD_UP), x+o, y+o,0); y += SCALE1(PILL_SIZE*2); - GFX_blitPillColor(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,0}, THEME_COLOR3, RGB_WHITE); + GFX_blitPillLight(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,0}); blitButton("D", screen, PAD_isPressed(BTN_DPAD_DOWN), x+o, y+o,0); x -= SCALE1(PILL_SIZE); y -= SCALE1(PILL_SIZE); - SDL_FillRect(screen, &(SDL_Rect){x+SCALE1(PILL_SIZE/2),y,SCALE1(PILL_SIZE*2),SCALE1(PILL_SIZE)}, THEME_COLOR3); + SDL_FillRect(screen, &(SDL_Rect){x+SCALE1(PILL_SIZE/2),y,SCALE1(PILL_SIZE*2),SCALE1(PILL_SIZE)}, THEME_COLOR2); - GFX_blitPillColor(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,0}, THEME_COLOR3, RGB_WHITE); + GFX_blitPillLight(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,0}); blitButton("L", screen, PAD_isPressed(BTN_DPAD_LEFT), x+o, y+o,0); x += SCALE1(PILL_SIZE*2); - GFX_blitPillColor(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,0}, THEME_COLOR3, RGB_WHITE); + GFX_blitPillLight(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,0}); blitButton("R", screen, PAD_isPressed(BTN_DPAD_RIGHT), x+o, y+o,0); } @@ -196,23 +201,21 @@ int main(int argc , char* argv[]) { int y = oy + SCALE1(PILL_SIZE*2); int o = SCALE1(BUTTON_MARGIN); - // SDL_FillRect(screen, &(SDL_Rect){x,y+SCALE1(PILL_SIZE/2),SCALE1(PILL_SIZE),SCALE1(PILL_SIZE*2)}, THEME_COLOR3); - GFX_blitPillColor(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,0}, THEME_COLOR3, RGB_WHITE); + GFX_blitPillLight(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,0}); blitButton("X", screen, PAD_isPressed(BTN_X), x+o, y+o,0); y += SCALE1(PILL_SIZE*2); - GFX_blitPillColor(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,0}, THEME_COLOR3, RGB_WHITE); + GFX_blitPillLight(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,0}); blitButton("B", screen, PAD_isPressed(BTN_B), x+o, y+o,0); x -= SCALE1(PILL_SIZE); y -= SCALE1(PILL_SIZE); - // SDL_FillRect(screen, &(SDL_Rect){x+SCALE1(PILL_SIZE/2),y,SCALE1(PILL_SIZE*2),SCALE1(PILL_SIZE)}, THEME_COLOR3); - GFX_blitPillColor(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,0}, THEME_COLOR3, RGB_WHITE); + GFX_blitPillLight(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,0}); blitButton("Y", screen, PAD_isPressed(BTN_Y), x+o, y+o,0); x += SCALE1(PILL_SIZE*2); - GFX_blitPillColor(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,0}, THEME_COLOR3, RGB_WHITE); + GFX_blitPillLight(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,0}); blitButton("A", screen, PAD_isPressed(BTN_A), x+o, y+o,0); } @@ -222,7 +225,7 @@ int main(int argc , char* argv[]) { int y = oy + SCALE1(PILL_SIZE); int w = SCALE1(42); - GFX_blitPillColor(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,SCALE1(98)}, THEME_COLOR3, RGB_WHITE); + GFX_blitPillLight(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,SCALE1(98)}); x += SCALE1(BUTTON_MARGIN); y += SCALE1(BUTTON_MARGIN); blitButton("VOL. -", screen, PAD_isPressed(BTN_MINUS), x, y, w); @@ -240,7 +243,7 @@ int main(int argc , char* argv[]) { int y = oy + SCALE1(PILL_SIZE * 3); int w = SCALE1(bw); - GFX_blitPillColor(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,SCALE1(pw)}, THEME_COLOR3, RGB_WHITE); + GFX_blitPillLight(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,SCALE1(pw)}); x += SCALE1(BUTTON_MARGIN); y += SCALE1(BUTTON_MARGIN); if (has_menu) { @@ -259,7 +262,7 @@ int main(int argc , char* argv[]) { int y = oy + SCALE1(PILL_SIZE * 5); int w = SCALE1(42); - GFX_blitPillColor(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,SCALE1(130)}, THEME_COLOR3, RGB_WHITE); + GFX_blitPillLight(ASSET_WHITE_PILL, screen, &(SDL_Rect){x,y,SCALE1(130)}); x += SCALE1(BUTTON_MARGIN); y += SCALE1(BUTTON_MARGIN); blitButton("SELECT", screen, PAD_isPressed(BTN_SELECT), x, y, w); @@ -267,7 +270,7 @@ int main(int argc , char* argv[]) { blitButton("START", screen, PAD_isPressed(BTN_START), x, y, w); x += w + SCALE1(BUTTON_MARGIN); - SDL_Surface* text = TTF_RenderUTF8_Blended(font.tiny, "QUIT", COLOR_LIGHT_TEXT); + SDL_Surface* text = TTF_RenderUTF8_Blended(font.tiny, "QUIT", uintToColour(THEME_COLOR6_255)); SDL_BlitSurface(text, NULL, screen, &(SDL_Rect){x,y+(SCALE1(BUTTON_SIZE)-text->h)/2}); SDL_FreeSurface(text); } @@ -282,7 +285,7 @@ int main(int argc , char* argv[]) { int cx = x + SCALE1(PILL_SIZE) / 2; int cy = y + SCALE1(PILL_SIZE) / 2; - fillCircle(screen, cx, cy, radius, THEME_COLOR3); + fillCircle(screen, cx, cy, radius, THEME_COLOR2); int dx = MAX(-travel, MIN(travel, (pad.laxis.x * travel) / STICK_AXIS_MAX)); int dy = MAX(-travel, MIN(travel, (pad.laxis.y * travel) / STICK_AXIS_MAX)); blitButton("L3", screen, PAD_isPressed(BTN_L3), x + o + dx, y + o + dy, 0); @@ -298,7 +301,7 @@ int main(int argc , char* argv[]) { int cx = x + SCALE1(PILL_SIZE) / 2; int cy = y + SCALE1(PILL_SIZE) / 2; - fillCircle(screen, cx, cy, radius, THEME_COLOR3); + fillCircle(screen, cx, cy, radius, THEME_COLOR2); int dx = MAX(-travel, MIN(travel, (pad.raxis.x * travel) / STICK_AXIS_MAX)); int dy = MAX(-travel, MIN(travel, (pad.raxis.y * travel) / STICK_AXIS_MAX)); blitButton("R3", screen, PAD_isPressed(BTN_R3), x+o+dx, y+o+dy,0); diff --git a/workspace/all/nextui/nextui.c b/workspace/all/nextui/nextui.c index 23d9e7323..38862e6a7 100644 --- a/workspace/all/nextui/nextui.c +++ b/workspace/all/nextui/nextui.c @@ -2894,7 +2894,7 @@ int main (int argc, char *argv[]) { else { SDL_Rect preview_rect = {ox,oy,screen->w,screen->h}; SDL_Surface * tmpsur = SDL_CreateRGBSurfaceWithFormat(0,screen->w,screen->h,screen->format->BitsPerPixel,screen->format->format); - SDL_FillRect(tmpsur, &preview_rect, CFG_getColor(COLOR_BACKGROUND)); + SDL_FillRect(tmpsur, &preview_rect, THEME_COLOR7); if(lastScreen == SCREEN_GAME) { GFX_animateSurfaceOpacity(tmpsur,0,0,screen->w,screen->h,255,0,CFG_getMenuTransitions() ? 150:20,LAYER_BACKGROUND); } else if(lastScreen == SCREEN_GAMELIST) { diff --git a/workspace/all/settings/makefile b/workspace/all/settings/makefile index da1aae07b..6338d32e0 100644 --- a/workspace/all/settings/makefile +++ b/workspace/all/settings/makefile @@ -21,9 +21,9 @@ SDL?=SDL TARGET = settings INCDIR = -I. -I../common/ -I../../$(PLATFORM)/platform/ -SOURCE = -c ../common/utils.c ../common/api.c ../common/config.c ../common/scaler.c ../common/http.c ../common/ra_auth.c ../common/ra_offline.c ../common/ra_sync.c ../common/ra_event_queue.c ../../$(PLATFORM)/platform/platform.c -CXXSOURCE = $(TARGET).cpp menu.cpp colorpickermenu.cpp wifimenu.cpp btmenu.cpp keyboardprompt.cpp -CXXSOURCE += build/$(PLATFORM)/utils.o build/$(PLATFORM)/api.o build/$(PLATFORM)/config.o build/$(PLATFORM)/scaler.o build/$(PLATFORM)/http.o build/$(PLATFORM)/ra_auth.o build/$(PLATFORM)/ra_offline.o build/$(PLATFORM)/ra_sync.o build/$(PLATFORM)/ra_event_queue.o build/$(PLATFORM)/platform.o +SOURCE = -c ../common/utils.c ../common/api.c ../common/config.c ../common/palette.c ../common/scaler.c ../common/http.c ../common/ra_auth.c ../common/ra_offline.c ../common/ra_sync.c ../common/ra_event_queue.c ../../$(PLATFORM)/platform/platform.c +CXXSOURCE = $(TARGET).cpp menu.cpp colorpickermenu.cpp palettemenu.cpp wifimenu.cpp btmenu.cpp keyboardprompt.cpp +CXXSOURCE += build/$(PLATFORM)/utils.o build/$(PLATFORM)/api.o build/$(PLATFORM)/config.o build/$(PLATFORM)/palette.o build/$(PLATFORM)/scaler.o build/$(PLATFORM)/http.o build/$(PLATFORM)/ra_auth.o build/$(PLATFORM)/ra_offline.o build/$(PLATFORM)/ra_sync.o build/$(PLATFORM)/ra_event_queue.o build/$(PLATFORM)/platform.o CC = $(CROSS_COMPILE)gcc CXX = $(CROSS_COMPILE)g++ @@ -50,7 +50,7 @@ PRODUCT= build/$(PLATFORM)/$(TARGET).elf all: $(PREFIX_LOCAL)/include/msettings.h mkdir -p build/$(PLATFORM) $(CC) $(SOURCE) $(CFLAGS) $(LDFLAGS) - mv utils.o api.o config.o scaler.o http.o ra_auth.o ra_offline.o ra_sync.o ra_event_queue.o platform.o build/$(PLATFORM) + mv utils.o api.o config.o palette.o scaler.o http.o ra_auth.o ra_offline.o ra_sync.o ra_event_queue.o platform.o build/$(PLATFORM) $(CXX) $(CXXSOURCE) -o $(PRODUCT) $(CXXFLAGS) $(LDFLAGS) -lstdc++ clean: rm -f $(PRODUCT) diff --git a/workspace/all/settings/menu.hpp b/workspace/all/settings/menu.hpp index 2c32d5e4a..2e3b98181 100644 --- a/workspace/all/settings/menu.hpp +++ b/workspace/all/settings/menu.hpp @@ -248,6 +248,11 @@ class MenuItem : public AbstractMenuItem virtual InputReactionHint handleInput(int &dirty) override; + // Re-sync the selected index from on_get(). Useful when the underlying value + // can change from outside this item (e.g. a palette selection invalidated by + // editing an individual color elsewhere in the menu). + void reselect() { initSelection(); } + const std::any getValue() const override { assert(valueIdx >= 0); diff --git a/workspace/all/settings/palettemenu.cpp b/workspace/all/settings/palettemenu.cpp new file mode 100644 index 000000000..a40296782 --- /dev/null +++ b/workspace/all/settings/palettemenu.cpp @@ -0,0 +1,92 @@ +#include "palettemenu.hpp" + +extern "C" { +#include "config.h" +#include "palette.h" +} + +namespace { + +// Menu row for picking a predefined color palette. Left/Right cycles through the +// available palettes (plus a "Custom" entry) and applies immediately; Confirm opens +// a submenu list. The displayed value is read live from CFG_getPaletteName() so it +// reflects "Custom" the moment an individual color is edited elsewhere. The empty +// string is the "Custom" value. +class PaletteMenuItem : public MenuItem +{ +public: + using MenuItem::MenuItem; + + const std::string getLabel() const override + { + std::string name; + if (on_get) + { + auto v = on_get(); + if (v.has_value()) + name = std::any_cast(v); + } + if (name.empty()) + return "Custom"; + // Show the persisted palette only if it is still available this session. + for (const auto &v : getValues()) + if (std::any_cast(v) == name) + return name; + return "Custom"; + } + + InputReactionHint handleInput(int &dirty) override + { + // Re-sync from the live palette name so Left/Right cycle from the correct + // position even after a color edit flipped us to Custom. + if (!isDeferred()) + reselect(); + return MenuItem::handleInput(dirty); + } +}; + +} // namespace + +AbstractMenuItem* buildPaletteMenuItem() +{ + // Enumerate once when the menu is built. + std::vector palettes(64); + int paletteCount = PALETTE_enumerate(palettes.data(), (int)palettes.size()); + palettes.resize(paletteCount); + + // Cycle values: "" == Custom, followed by each palette name. + std::vector palette_values; + std::vector palette_labels; + palette_values.push_back(std::string("")); + palette_labels.push_back("Custom"); + for (const auto &p : palettes) { + palette_values.push_back(std::string(p.name)); + palette_labels.push_back(p.name); + } + + // Submenu: a plain list; selecting a row applies it and closes. + std::vector paletteSubItems; + paletteSubItems.push_back(new MenuItem{ListItemType::Button, "Custom", "", + [](AbstractMenuItem &) -> InputReactionHint { CFG_selectCustomPalette(); return Exit; }}); + for (const auto &p : palettes) { + ColorPalette palette = p; // capture a copy for the closure + paletteSubItems.push_back(new MenuItem{ListItemType::Button, p.name, "", + [palette](AbstractMenuItem &) -> InputReactionHint { PALETTE_apply(&palette); return Exit; }}); + } + auto *paletteSubmenu = new MenuList(MenuItemType::Fixed, "Color Palette", std::move(paletteSubItems)); + + // Selector row. on_set applies the palette named by the cycled value ("" == Custom). + std::vector palettesForSetter = palettes; + return new PaletteMenuItem{ListItemType::Generic, "Color Palette", + "Pick a predefined color palette or edit the colors below.", + palette_values, palette_labels, + []() -> std::any { return std::string(CFG_getPaletteName()); }, + [palettesForSetter](const std::any &v) { + std::string name = std::any_cast(v); + if (name.empty()) { CFG_selectCustomPalette(); return; } + for (const auto &p : palettesForSetter) + if (name == p.name) { PALETTE_apply(&p); return; } + }, + []() { CFG_clearPalette(); }, // "Reset to defaults": detach only, colors reset by each color item's own on_reset + DeferToSubmenu, paletteSubmenu}; +} diff --git a/workspace/all/settings/palettemenu.hpp b/workspace/all/settings/palettemenu.hpp new file mode 100644 index 000000000..6d0d25061 --- /dev/null +++ b/workspace/all/settings/palettemenu.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "menu.hpp" + +// Builds the "Color Palette" appearance-menu row: cycling Left/Right steps through +// "Custom" plus every enumerated ColorPalette and applies immediately; Confirm opens +// a submenu with the same choices for direct selection. The row's displayed value +// tracks CFG_getPaletteName() live, so it falls back to "Custom" the moment an +// individual color is edited elsewhere in the menu. +AbstractMenuItem* buildPaletteMenuItem(); diff --git a/workspace/all/settings/settings.cpp b/workspace/all/settings/settings.cpp index 442855cfe..3e8185482 100644 --- a/workspace/all/settings/settings.cpp +++ b/workspace/all/settings/settings.cpp @@ -25,6 +25,7 @@ extern "C" #include "btmenu.hpp" #include "keyboardprompt.hpp" #include "colorpickermenu.hpp" +#include "palettemenu.hpp" #define BUSYBOX_STOCK_VERSION "1.27.2" @@ -192,9 +193,9 @@ namespace { {1, "Main Color", "The color used to render main UI elements.", CFG_DEFAULT_COLOR1}, {2, "Primary Accent Color", "The color used to highlight important things in the user interface.", CFG_DEFAULT_COLOR2}, {3, "Secondary Accent Color", "A secondary highlight color.", CFG_DEFAULT_COLOR3}, - {6, "Hint info Color", "Color for button hints and info", CFG_DEFAULT_COLOR6}, {4, "List Text", "List text color", CFG_DEFAULT_COLOR4}, {5, "List Text Selected", "List selected text color", CFG_DEFAULT_COLOR5}, + {6, "Hint info Color", "Color for button hints and info", CFG_DEFAULT_COLOR6}, {7, "Background Color", "Background color used when no background image is set.", CFG_DEFAULT_COLOR7}, }; @@ -371,9 +372,10 @@ int main(int argc, char *argv[]) tz_labels.push_back(std::string(timezones[i])); } - // Factory helpers to avoid repeating identical lambda boilerplate for each picker + // Factory helpers to avoid repeating identical lambda boilerplate for each picker. + // Editing an individual color detaches from any predefined palette ("Custom"). auto makeColorSetter = [](int id) -> ValueSetCallback { - return [id](const std::any &v){ CFG_setColor(id, std::any_cast(v)); }; + return [id](const std::any &v){ CFG_setColor(id, std::any_cast(v)); CFG_clearPalette(); }; }; auto makeColorOpener = [](ColorPickerMenu *picker, int id, std::string name) -> MenuListCallback { return [picker, id, name](AbstractMenuItem &item) -> InputReactionHint { @@ -385,7 +387,7 @@ int main(int argc, char *argv[]) return [id]() -> std::any { return CFG_getColor(id); }; }; auto makeColorResetter = [](int id, uint32_t defaultColor) -> ValueResetCallback { - return [id, defaultColor]() { CFG_setColor(id, defaultColor); }; + return [id, defaultColor]() { CFG_setColor(id, defaultColor); CFG_clearPalette(); }; }; // Pre-create one RGBA picker per color setting (reused across opens) @@ -426,6 +428,7 @@ int main(int argc, char *argv[]) []() -> std::any { return CFG_getFontStyle(); }, [](const std::any &value) { CFG_setFontStyle(std::any_cast(value)); }, []() { CFG_setFontStyle(CFG_DEFAULT_FONT_STYLE); }}); + appearanceItems.push_back(buildPaletteMenuItem()); for (auto *item : colorMenuItems) appearanceItems.push_back(item); appearanceItems.push_back(new MenuItem{ListItemType::Generic, "Show battery percentage", "Show battery level as percent in the status pill", {false, true}, on_off,