diff --git a/workspace/all/settings/colorpickermenu.cpp b/workspace/all/settings/colorpickermenu.cpp new file mode 100644 index 000000000..3c630003c --- /dev/null +++ b/workspace/all/settings/colorpickermenu.cpp @@ -0,0 +1,395 @@ +#include "colorpickermenu.hpp" + +#include + +static constexpr int NUM_SLIDERS = 3; + +static void decodeColor(uint32_t c, int &r, int &g, int &b) +{ + r = (c >> 16) & 0xFF; + g = (c >> 8) & 0xFF; + b = c & 0xFF; +} + +/////////////////////////////////////////////////////////// +// ColorPickerMenu + +ColorPickerMenu::ColorPickerMenu(uint32_t initialColor, ValueSetCallback on_set, + std::vector presets, std::string label) + : MenuList(MenuItemType::Custom, "", {}), on_set(std::move(on_set)), + presets(std::move(presets)), label_(std::move(label)), originalColor_(initialColor), + selected(0) +{ + decodeColor(initialColor, r, g, b); +} + +void ColorPickerMenu::reset(uint32_t color, std::vector newPresets, std::string label) +{ + decodeColor(color, r, g, b); + originalColor_ = color; + selected = 0; + presets = std::move(newPresets); + label_ = std::move(label); +} + +uint32_t ColorPickerMenu::currentColor() const +{ + return ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b; +} + +void ColorPickerMenu::applyColor() +{ + if (on_set) + on_set(currentColor()); +} + +InputReactionHint ColorPickerMenu::handleInput(int &dirty, int &quit) +{ + int total = NUM_SLIDERS + (int)presets.size(); + + if (PAD_justRepeated(BTN_UP)) + { + if (selected > 0) + { + selected--; + dirty = 1; + } + return NoOp; + } + else if (PAD_justRepeated(BTN_DOWN)) + { + if (selected < total - 1) + { + selected++; + dirty = 1; + } + return NoOp; + } + else if (PAD_justPressed(BTN_B)) + { + decodeColor(originalColor_, r, g, b); + applyColor(); + dirty = 1; + quit = 1; + return NoOp; + } + else if (PAD_justPressed(BTN_X)) + { + originalColor_ = currentColor(); + dirty = 1; + return NoOp; + } + + if (selected < NUM_SLIDERS) + { + int *channels[] = {&r, &g, &b}; + int *channel = channels[selected]; + if (PAD_justRepeated(BTN_LEFT)) + { + *channel = std::max(0, *channel - 1); + applyColor(); + dirty = 1; + return NoOp; + } + else if (PAD_justRepeated(BTN_RIGHT)) + { + *channel = std::min(255, *channel + 1); + applyColor(); + dirty = 1; + return NoOp; + } + else if (PAD_justRepeated(BTN_L1)) + { + *channel = std::max(0, *channel - 16); + applyColor(); + dirty = 1; + return NoOp; + } + else if (PAD_justRepeated(BTN_R1)) + { + *channel = std::min(255, *channel + 16); + applyColor(); + dirty = 1; + return NoOp; + } + } + else + { + int presetIdx = selected - NUM_SLIDERS; + if (PAD_justPressed(BTN_A) && presetIdx < (int)presets.size()) + { + const auto &preset = presets[presetIdx]; + decodeColor(preset.color, r, g, b); + applyColor(); + selected = 0; + dirty = 1; + return NoOp; + } + } + + return Unhandled; +} + +static void drawSolidRoundedRect(SDL_Surface *surface, const SDL_Rect &rect, int radius, uint32_t color) +{ + int r = std::max(0, std::min(radius, std::min(rect.w, rect.h) / 2)); + for (int row = 0; row < rect.h; row++) + { + int x_clip = 0; + if (row < r) { + float dx = sqrtf((float)(2 * r * row - row * row)); + x_clip = r - (int)dx; + } else if (row >= rect.h - r) { + int i = rect.h - 1 - row; + float dx = sqrtf((float)(2 * r * i - i * i)); + x_clip = r - (int)dx; + } + int x0 = rect.x + x_clip; + int span = rect.w - 2 * x_clip; + if (span <= 0) continue; + SDL_Rect l = {x0, rect.y + row, span, 1}; + SDL_FillRect(surface, &l, color); + } +} + +static void drawRoundedRect(SDL_Surface *surface, const SDL_Rect &rect, int radius, + uint32_t outer, uint32_t middle, uint32_t fill) +{ + drawSolidRoundedRect(surface, rect, radius, outer); + if (rect.w > 2 && rect.h > 2) { + SDL_Rect r1 = {rect.x+1, rect.y+1, rect.w-2, rect.h-2}; + drawSolidRoundedRect(surface, r1, std::max(0, radius-1), middle); + } + if (rect.w > 4 && rect.h > 4) { + SDL_Rect r2 = {rect.x+2, rect.y+2, rect.w-4, rect.h-4}; + drawSolidRoundedRect(surface, r2, std::max(0, radius-2), fill); + } +} + +static void drawGradientCapsule(SDL_Surface *surface, const SDL_Rect &bar, + int r, int g, int b, int channel) +{ + if (bar.w <= 0 || bar.h <= 0) return; + const int R = bar.h / 2; + + for (int x = 0; x < bar.w; x++) + { + int ch_val = (bar.w > 1) ? (int)((float)x / (bar.w - 1) * 255.0f) : 128; + uint8_t cr = (channel == 0) ? (uint8_t)ch_val : (uint8_t)r; + uint8_t cg = (channel == 1) ? (uint8_t)ch_val : (uint8_t)g; + uint8_t cb = (channel == 2) ? (uint8_t)ch_val : (uint8_t)b; + uint32_t col = SDL_MapRGB(surface->format, cr, cg, cb); + + int y_inset = 0; + if (R > 0) { + if (x < R) { + int dx = R - x; + float dy = sqrtf((float)(R * R - dx * dx)); + y_inset = R - (int)dy; + } else if (x >= bar.w - R) { + int dx = x - (bar.w - 1 - R); + float inner = (float)(R * R - dx * dx); + if (inner < 0.0f) continue; + y_inset = R - (int)sqrtf(inner); + } + } + + int col_h = bar.h - 2 * y_inset; + if (col_h <= 0) continue; + SDL_Rect col_rect = {bar.x + x, bar.y + y_inset, 1, col_h}; + SDL_FillRect(surface, &col_rect, col); + } +} + +void ColorPickerMenu::drawSlider(SDL_Surface *surface, const SDL_Rect &row, + const char *label, int value, bool is_selected, int channel) +{ + if (is_selected) + drawSolidRoundedRect(surface, row, row.h / 2, THEME_COLOR1); + + SDL_Color text_color = is_selected ? uintToColour(THEME_COLOR5_255) : uintToColour(THEME_COLOR4_255); + + // Fixed-width slots so all sliders align regardless of rendered glyph widths + int label_fixed_w; + { + int wr, wg, wb; + TTF_SizeUTF8(font.small, "R", &wr, nullptr); + TTF_SizeUTF8(font.small, "G", &wg, nullptr); + TTF_SizeUTF8(font.small, "B", &wb, nullptr); + label_fixed_w = std::max({wr, wg, wb}); + } + int hex_fixed_w; + TTF_SizeUTF8(font.tiny, "00", &hex_fixed_w, nullptr); + + // Channel label ("R", "G", "B") — centered in fixed-width slot + SDL_Surface *label_surf = TTF_RenderUTF8_Blended(font.small, label, text_color); + int label_x = row.x + SCALE1(OPTION_PADDING) + (label_fixed_w - label_surf->w) / 2; + SDL_BlitSurfaceCPP(label_surf, {}, surface, + {label_x, row.y + (row.h - label_surf->h) / 2}); + SDL_FreeSurface(label_surf); + + // Hex value right-aligned in fixed-width slot + char hex_str[3]; + snprintf(hex_str, sizeof(hex_str), "%02X", value); + SDL_Surface *hex_surf = TTF_RenderUTF8_Blended(font.tiny, hex_str, text_color); + int hex_slot_x = row.x + row.w - SCALE1(OPTION_PADDING) - hex_fixed_w; + SDL_BlitSurfaceCPP(hex_surf, {}, surface, + {hex_slot_x + (hex_fixed_w - hex_surf->w), + row.y + (row.h - hex_surf->h) / 2}); + SDL_FreeSurface(hex_surf); + + // Slider bar — stable bounds derived from fixed label/hex widths + int bar_x = row.x + SCALE1(OPTION_PADDING) + label_fixed_w + SCALE1(OPTION_PADDING); + int bar_w = row.w - SCALE1(OPTION_PADDING) - label_fixed_w - SCALE1(OPTION_PADDING * 3) - hex_fixed_w; + int bar_h = SCALE1(12); + int bar_y = row.y + (row.h - bar_h) / 2; + + if (bar_w > 0) + { + uint32_t white = SDL_MapRGB(surface->format, 255, 255, 255); + uint32_t black = SDL_MapRGB(surface->format, 0, 0, 0); + + // Ring inset within bar bounds: 1px black outer, 1px white inner, gradient fill + drawSolidRoundedRect(surface, {bar_x, bar_y, bar_w, bar_h}, bar_h / 2, black); + if (bar_w > 2 && bar_h > 2) + drawSolidRoundedRect(surface, {bar_x + 1, bar_y + 1, bar_w - 2, bar_h - 2}, + (bar_h - 2) / 2, white); + if (bar_w > 4 && bar_h > 4) + drawGradientCapsule(surface, {bar_x + 2, bar_y + 2, bar_w - 4, bar_h - 4}, + this->r, this->g, this->b, channel); + + // Circle thumb indicator + const int circ_d = bar_h + SCALE1(2); + const int circ_border = SCALE1(1); + + int cx = bar_x + (int)((float)value / 255.0f * (bar_w - 1)); + int cy = bar_y + bar_h / 2; + cx = std::max(bar_x + circ_d / 2, std::min(bar_x + bar_w - 1 - circ_d / 2, cx)); + + // Draw the thumb onto a temporary ARGB surface so alpha blending works. + // SDL_FillRect writes directly (no blend); blitting FROM an alpha surface does blend. + SDL_Surface *thumb = SDL_CreateRGBSurfaceWithFormat(0, circ_d, circ_d, + 32, SDL_PIXELFORMAT_ARGB8888); + SDL_SetSurfaceBlendMode(thumb, SDL_BLENDMODE_BLEND); + SDL_SetColorKey(thumb, SDL_FALSE, 0); + SDL_FillRect(thumb, nullptr, SDL_MapRGBA(thumb->format, 0, 0, 0, 0)); + + SDL_Rect thumb_outer = {0, 0, circ_d, circ_d}; + uint32_t thumbB = SDL_MapRGBA(thumb->format, 0, 0, 0, 200); + drawSolidRoundedRect(thumb, thumb_outer, circ_d / 2, thumbB); + int inner_d = circ_d - circ_border * 2; + SDL_Rect thumb_inner = {circ_border, circ_border, inner_d, inner_d}; + uint32_t thumbF = SDL_MapRGBA(thumb->format, 255, 255, 255, 200); + drawSolidRoundedRect(thumb, thumb_inner, inner_d / 2, thumbF); + + SDL_Rect outer_rect = {cx - circ_d / 2, cy - circ_d / 2, circ_d, circ_d}; + SDL_BlitSurface(thumb, nullptr, surface, &outer_rect); + SDL_FreeSurface(thumb); + } +} + +void ColorPickerMenu::drawPreset(SDL_Surface *surface, const SDL_Rect &row, + const ColorPreset &preset, bool is_selected) +{ + if (is_selected) + drawSolidRoundedRect(surface, row, row.h / 2, THEME_COLOR1); + + // Small color square with white border + int sq = SCALE1(FONT_TINY); + SDL_Rect sq_rect = {row.x + SCALE1(OPTION_PADDING), row.y + (row.h - sq) / 2, sq, sq}; + SDL_FillRect(surface, &sq_rect, SDL_MapRGB(surface->format, 255, 255, 255)); + SDL_Rect sq_inner = {sq_rect.x + 1, sq_rect.y + 1, sq_rect.w - 2, sq_rect.h - 2}; + SDL_FillRect(surface, &sq_inner, SDL_MapRGB(surface->format, + (preset.color >> 16) & 0xFF, (preset.color >> 8) & 0xFF, preset.color & 0xFF)); + + SDL_Color text_color = is_selected ? uintToColour(THEME_COLOR5_255) : uintToColour(THEME_COLOR4_255); + + // Hex value "#RRGGBB" + char hex_str[8]; + snprintf(hex_str, sizeof(hex_str), "#%06X", preset.color); + SDL_Surface *hex_surf = TTF_RenderUTF8_Blended(font.tiny, hex_str, text_color); + int hex_x = sq_rect.x + sq + SCALE1(OPTION_PADDING / 2 + 2); + SDL_BlitSurfaceCPP(hex_surf, {}, surface, + {hex_x, row.y + (row.h - hex_surf->h) / 2}); + int hex_w = hex_surf->w; + SDL_FreeSurface(hex_surf); + + // Name label + SDL_Surface *name_surf = TTF_RenderUTF8_Blended(font.tiny, preset.label.c_str(), text_color); + SDL_BlitSurfaceCPP(name_surf, {}, surface, + {hex_x + hex_w + SCALE1(OPTION_PADDING), + row.y + (row.h - name_surf->h) / 2}); + SDL_FreeSurface(name_surf); +} + +void ColorPickerMenu::drawCustom(SDL_Surface *surface, const SDL_Rect &dst) +{ + // Title text — drawn one PILL_SIZE above the slider area (same row as the clock) + { + char display_name[256]; + GFX_truncateText(font.large, label_.c_str(), display_name, + (surface->w - SCALE1(PADDING * 2)) / 2, + SCALE1(BUTTON_PADDING * 2)); + SDL_Surface *title_text = TTF_RenderUTF8_Blended(font.large, display_name, + uintToColour(THEME_COLOR4_255)); + int title_y = dst.y - SCALE1(PILL_SIZE) + (SCALE1(PILL_SIZE) - title_text->h) / 2; + SDL_BlitSurfaceCPP(title_text, {}, surface, {dst.x, title_y}); + SDL_FreeSurface(title_text); + } + + // Button hints at the bottom of the screen + { + char *left_hints[] = {(char *)"B", (char *)"BACK", (char *)"X", (char *)"SAVE", nullptr}; + GFX_blitButtonGroup(left_hints, 0, surface, 0); + + if (selected < NUM_SLIDERS) + { + char *right_hints[] = {(char *)"L/R", (char *)"FINE", (char *)"L1/R1", (char *)"COARSE", nullptr}; + GFX_blitButtonGroup(right_hints, 0, surface, 1); + } + else + { + char *right_hints[] = {(char *)"A", (char *)"APPLY", nullptr}; + GFX_blitButtonGroup(right_hints, 0, surface, 1); + } + } + + const int TOP_OFFSET = SCALE1(4); + const int PREVIEW_SIZE = SCALE1(BUTTON_SIZE * 2 + PADDING); + const int SLIDER_AREA_W = dst.w - PREVIEW_SIZE - SCALE1(PADDING); + const int PREVIEW_RADIUS = SCALE1(4); + + uint32_t white = SDL_MapRGB(surface->format, 255, 255, 255); + uint32_t black = SDL_MapRGB(surface->format, 0, 0, 0); + uint32_t col_mapped = SDL_MapRGB(surface->format, r, g, b); + + // R, G, B slider rows + const char *channel_labels[] = {"R", "G", "B"}; + int channel_values[] = {r, g, b}; + for (int i = 0; i < NUM_SLIDERS; i++) + { + SDL_Rect row = {dst.x, dst.y + TOP_OFFSET + SCALE1(i * BUTTON_SIZE), SLIDER_AREA_W, SCALE1(BUTTON_SIZE)}; + drawSlider(surface, row, channel_labels[i], channel_values[i], selected == i, i); + } + + // Color preview square to the right of sliders, centered vertically in the slider area + SDL_Rect preview = { + dst.x + SLIDER_AREA_W + SCALE1(PADDING), + dst.y + TOP_OFFSET + (SCALE1(BUTTON_SIZE * NUM_SLIDERS) - PREVIEW_SIZE) / 2, + PREVIEW_SIZE, + PREVIEW_SIZE + }; + drawRoundedRect(surface, preview, PREVIEW_RADIUS, black, white, col_mapped); + + // Preset rows below the sliders + for (int i = 0; i < (int)presets.size(); i++) + { + SDL_Rect row = { + dst.x, + dst.y + TOP_OFFSET + SCALE1(BUTTON_SIZE * NUM_SLIDERS) + SCALE1(i * BUTTON_SIZE), + dst.w, + SCALE1(BUTTON_SIZE) + }; + drawPreset(surface, row, presets[i], selected == NUM_SLIDERS + i); + } +} diff --git a/workspace/all/settings/colorpickermenu.hpp b/workspace/all/settings/colorpickermenu.hpp new file mode 100644 index 000000000..561c5acd0 --- /dev/null +++ b/workspace/all/settings/colorpickermenu.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "menu.hpp" + +struct ColorPreset { + uint32_t color; // 0xRRGGBB + std::string label; +}; + +class ColorPickerMenu : public MenuList +{ + int r, g, b; + int selected; + ValueSetCallback on_set; + std::vector presets; + std::string label_; + uint32_t originalColor_; + + uint32_t currentColor() const; + void applyColor(); + void drawSlider(SDL_Surface *surface, const SDL_Rect &row, + const char *label, int value, bool is_selected, int channel); + void drawPreset(SDL_Surface *surface, const SDL_Rect &row, + const ColorPreset &preset, bool is_selected); + +public: + ColorPickerMenu(uint32_t initialColor, ValueSetCallback on_set, + std::vector presets, std::string label); + + void reset(uint32_t color, std::vector newPresets, std::string label); + + InputReactionHint handleInput(int &dirty, int &quit) override; + void drawCustom(SDL_Surface *surface, const SDL_Rect &dst) override; +}; diff --git a/workspace/all/settings/makefile b/workspace/all/settings/makefile index 32cbbce79..9b0927114 100644 --- a/workspace/all/settings/makefile +++ b/workspace/all/settings/makefile @@ -22,7 +22,7 @@ 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 ../../$(PLATFORM)/platform/platform.c -CXXSOURCE = $(TARGET).cpp menu.cpp wifimenu.cpp btmenu.cpp keyboardprompt.cpp +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)/platform.o CC = $(CROSS_COMPILE)gcc diff --git a/workspace/all/settings/menu.cpp b/workspace/all/settings/menu.cpp index 98574dcc8..724ae2c30 100644 --- a/workspace/all/settings/menu.cpp +++ b/workspace/all/settings/menu.cpp @@ -7,6 +7,7 @@ extern "C" #include "utils.h" } +#include #include #include typedef std::shared_mutex Lock; @@ -719,7 +720,13 @@ void MenuList::drawFixedItem(SDL_Surface *surface, const SDL_Rect &dst, const Ab if (item.getType() == ListItemType::Color) { - uint32_t color = mapUint(surface, std::any_cast(item.getValue())); + // Read the live color directly from on_get() so the swatch and hex + // label always reflect the current value, even after the RGB picker + // sets an arbitrary color that is not in the predefined palette. + uint32_t rawColor = item.on_get + ? std::any_cast(item.on_get()) + : std::any_cast(item.getValue()); + uint32_t color = mapUint(surface, rawColor); SDL_Rect rect = { dst.x + dst.w - SCALE1(OPTION_PADDING + FONT_TINY), dst.y + SCALE1(BUTTON_SIZE - FONT_TINY) / 2, @@ -730,6 +737,11 @@ void MenuList::drawFixedItem(SDL_Surface *surface, const SDL_Rect &dst, const Ab rect.w -= 1; SDL_FillRect(surface, &rect, color); #define COLOR_PADDING 4 + // Rerender the label from the live hex value + SDL_FreeSurface(text); + char hexLabel[12]; + snprintf(hexLabel, sizeof(hexLabel), "0x%06X", rawColor); + text = TTF_RenderUTF8_Blended(font.tiny, hexLabel, text_color_value); SDL_BlitSurfaceCPP(text, {}, surface, {dst.x + mw - text->w - SCALE1(OPTION_PADDING + COLOR_PADDING + FONT_TINY), dst.y + ((dst.h - text->h) / 2)}); } else if(item.getType() == ListItemType::Button) { @@ -917,6 +929,7 @@ bool MenuList::isOverlayVisible() return overlayVisible; } + static void drawOverlayLocal(SDL_Surface* screen) { // ReadLock r(overlayLock); // Assumes caller held lock or is safe if (!overlayVisible) return; diff --git a/workspace/all/settings/menu.hpp b/workspace/all/settings/menu.hpp index ead45e0e7..e8a0e7310 100644 --- a/workspace/all/settings/menu.hpp +++ b/workspace/all/settings/menu.hpp @@ -373,4 +373,5 @@ const MenuListCallback DeferToSubmenu = [](AbstractMenuItem &itm) -> InputReacti const MenuListCallback ResetCurrentMenu = [](AbstractMenuItem &itm) -> InputReactionHint { return InputReactionHint::ResetAllItems; -}; \ No newline at end of file +}; + diff --git a/workspace/all/settings/settings.cpp b/workspace/all/settings/settings.cpp index 6710e00ce..00725b0fa 100644 --- a/workspace/all/settings/settings.cpp +++ b/workspace/all/settings/settings.cpp @@ -10,11 +10,13 @@ extern "C" #include #include +#include #include #include #include "wifimenu.hpp" #include "btmenu.hpp" #include "keyboardprompt.hpp" +#include "colorpickermenu.hpp" #define BUSYBOX_STOCK_VERSION "1.27.2" @@ -122,6 +124,28 @@ static const std::vector ra_sort_labels = { }; namespace { + struct ColorDef { int id; const char *name; const char *desc; uint32_t defaultColor; }; + static const ColorDef g_colorDefs[] = { + {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}, + {7, "Background Color", "Background color used when no background image is set.", CFG_DEFAULT_COLOR7}, + }; + + static std::vector buildColorPresets(int excludeId) + { + std::vector result; + for (const auto &def : g_colorDefs) + { + if (def.id != excludeId) + result.push_back({CFG_getColor(def.id), def.name}); + } + return result; + } + std::string execCommand(const char* cmd) { std::array buffer; std::string result; @@ -275,99 +299,105 @@ int main(int argc, char *argv[]) tz_labels.push_back(std::string(timezones[i])); } - auto appearanceMenu = new MenuList(MenuItemType::Fixed, "Appearance", - {new MenuItem{ListItemType::Generic, "Font", "The font to render all UI text.", {0, 1}, font_names, - []() -> std::any{ return CFG_getFontId(); }, - [](const std::any &value){ CFG_setFontId(std::any_cast(value)); }, - []() { CFG_setFontId(CFG_DEFAULT_FONT_ID);}}, - new MenuItem{ListItemType::Color, "Main Color", "The color used to render main UI elements.", colors, color_strings, - []() -> std::any{ return CFG_getColor(COLOR_MAIN); }, - [](const std::any &value){ CFG_setColor(COLOR_MAIN, std::any_cast(value)); }, - []() { CFG_setColor(COLOR_MAIN, CFG_DEFAULT_COLOR1);}}, - new MenuItem{ListItemType::Color, "Primary Accent Color", "The color used to highlight important things in the user interface.", colors, color_strings, - []() -> std::any{ return CFG_getColor(COLOR_ACCENT); }, - [](const std::any &value){ CFG_setColor(COLOR_ACCENT, std::any_cast(value)); }, - []() { CFG_setColor(COLOR_ACCENT, CFG_DEFAULT_COLOR2);}}, - new MenuItem{ListItemType::Color, "Secondary Accent Color", "A secondary highlight color.", colors, color_strings, - []() -> std::any{ return CFG_getColor(COLOR_ACCENT2); }, - [](const std::any &value){ CFG_setColor(COLOR_ACCENT2, std::any_cast(value)); }, - []() { CFG_setColor(COLOR_ACCENT2, CFG_DEFAULT_COLOR3);}}, - new MenuItem{ListItemType::Color, "Hint info Color", "Color for button hints and info", colors, color_strings, - []() -> std::any{ return CFG_getColor(COLOR_HINT); }, - [](const std::any &value){ CFG_setColor(COLOR_HINT, std::any_cast(value)); }, - []() { CFG_setColor(COLOR_HINT, CFG_DEFAULT_COLOR6);}}, - new MenuItem{ListItemType::Color, "List Text", "List text color", colors, color_strings, - []() -> std::any{ return CFG_getColor(COLOR_LIST_TEXT); }, - [](const std::any &value){ CFG_setColor(COLOR_LIST_TEXT, std::any_cast(value)); }, - []() { CFG_setColor(COLOR_LIST_TEXT, CFG_DEFAULT_COLOR4);}}, - new MenuItem{ListItemType::Color, "List Text Selected", "List selected text color", colors, color_strings, - []() -> std::any { return CFG_getColor(COLOR_LIST_TEXT_SELECTED); }, - [](const std::any &value) { CFG_setColor(COLOR_LIST_TEXT_SELECTED, std::any_cast(value)); }, - []() { CFG_setColor(COLOR_LIST_TEXT_SELECTED, CFG_DEFAULT_COLOR5);}}, - new MenuItem{ListItemType::Color, "Background Color", "Background color used when no background image is set.", colors, color_strings, - []() -> std::any { return CFG_getColor(COLOR_BACKGROUND); }, - [](const std::any &value) { CFG_setColor(COLOR_BACKGROUND, std::any_cast(value)); }, - []() { CFG_setColor(COLOR_BACKGROUND, CFG_DEFAULT_COLOR7);}}, - new MenuItem{ListItemType::Generic, "Show battery percentage", "Show battery level as percent in the status pill", {false, true}, on_off, - []() -> std::any { return CFG_getShowBatteryPercent(); }, - [](const std::any &value) { CFG_setShowBatteryPercent(std::any_cast(value)); }, - []() { CFG_setShowBatteryPercent(CFG_DEFAULT_SHOWBATTERYPERCENT);}}, - new MenuItem{ListItemType::Generic, "Show menu animations", "Enable or disable menu animations", {false, true}, on_off, - []() -> std::any{ return CFG_getMenuAnimations(); }, - [](const std::any &value) { CFG_setMenuAnimations(std::any_cast(value)); }, - []() { CFG_setMenuAnimations(CFG_DEFAULT_SHOWMENUANIMATIONS);}}, - new MenuItem{ListItemType::Generic, "Show menu transitions", "Enable or disable animated transitions", {false, true}, on_off, - []() -> std::any{ return CFG_getMenuTransitions(); }, - [](const std::any &value) { CFG_setMenuTransitions(std::any_cast(value)); }, - []() { CFG_setMenuTransitions(CFG_DEFAULT_SHOWMENUTRANSITIONS);}}, - new MenuItem{ListItemType::Generic, "Game art corner radius", "Set the radius for the rounded corners of game art", 0, 24, "px", - []() -> std::any{ return CFG_getThumbnailRadius(); }, - [](const std::any &value) { CFG_setThumbnailRadius(std::any_cast(value)); }, - []() { CFG_setThumbnailRadius(CFG_DEFAULT_THUMBRADIUS);}}, - new MenuItem{ListItemType::Generic, "Game art width", "Set the percentage of screen width used for game art.\nUI elements might overrule this to avoid clipping.", - 5, 100, "%", - []() -> std::any{ return (int)(CFG_getGameArtWidth() * 100); }, - [](const std::any &value) { CFG_setGameArtWidth((double)std::any_cast(value) / 100.0); }, - []() { CFG_setGameArtWidth(CFG_DEFAULT_GAMEARTWIDTH);}}, - new MenuItem{ListItemType::Generic, "Show folder names at root", "Show folder names at root directory", {false, true}, on_off, - []() -> std::any { return CFG_getShowFolderNamesAtRoot(); }, - [](const std::any &value) { CFG_setShowFolderNamesAtRoot(std::any_cast(value)); }, - []() { CFG_setShowFolderNamesAtRoot(CFG_DEFAULT_SHOWFOLDERNAMESATROOT);}}, - new MenuItem{ListItemType::Generic, "Show Recents", "Show \"Recently Played\" menu entry in game list.", {false, true}, on_off, - []() -> std::any { return CFG_getShowRecents(); }, - [](const std::any &value) { CFG_setShowRecents(std::any_cast(value)); }, - []() { CFG_setShowRecents(CFG_DEFAULT_SHOWRECENTS);}}, - new MenuItem{ListItemType::Generic, "Show Tools", "Show \"Tools\" menu entry in game list.", {false, true}, on_off, - []() -> std::any { return CFG_getShowTools(); }, - [](const std::any &value) { CFG_setShowTools(std::any_cast(value)); }, - []() { CFG_setShowTools(CFG_DEFAULT_SHOWTOOLS);}}, - new MenuItem{ListItemType::Generic, "Show Collections", "Show \"Collections\" menu entry in game list.", {false, true}, on_off, - []() -> std::any { return CFG_getShowCollections(); }, - [](const std::any &value) { CFG_setShowCollections(std::any_cast(value)); }, - []() { CFG_setShowCollections(CFG_DEFAULT_SHOWCOLLECTIONS);}}, - new MenuItem{ListItemType::Generic, "Show game art", "Show game artwork in the main menu", {false, true}, on_off, []() -> std::any - { return CFG_getShowGameArt(); }, - [](const std::any &value) - { CFG_setShowGameArt(std::any_cast(value)); }, - []() { CFG_setShowGameArt(CFG_DEFAULT_SHOWGAMEART);}}, - new MenuItem{ListItemType::Generic, "Use folder background for ROMs", "If enabled, used the emulator background image. Otherwise uses the default.", {false, true}, on_off, []() -> std::any - { return CFG_getRomsUseFolderBackground(); }, - [](const std::any &value) - { CFG_setRomsUseFolderBackground(std::any_cast(value)); }, - []() { CFG_setRomsUseFolderBackground(CFG_DEFAULT_ROMSUSEFOLDERBACKGROUND);}}, - new MenuItem{ListItemType::Generic, "Show Quickswitcher UI", "Show/hide Quickswitcher UI elements.\nWhen hidden, will only draw background images.", {false, true}, on_off, - []() -> std::any{ return CFG_getShowQuickswitcherUI(); }, - [](const std::any &value){ CFG_setShowQuickswitcherUI(std::any_cast(value)); }, - []() { CFG_setShowQuickswitcherUI(CFG_DEFAULT_SHOWQUICKWITCHERUI);}}, - // not needed anymore - // new MenuItem{ListItemType::Generic, "Game switcher scaling", "The scaling algorithm used to display the savegame image.", scaling, scaling_strings, []() -> std::any - // { return CFG_getGameSwitcherScaling(); }, - // [](const std::any &value) - // { CFG_setGameSwitcherScaling(std::any_cast(value)); }, - // []() { CFG_setGameSwitcherScaling(CFG_DEFAULT_GAMESWITCHERSCALING);}}, - - new MenuItem{ListItemType::Button, "Reset to defaults", "Resets all options in this menu to their default values.", ResetCurrentMenu}, - }); + // Factory helpers to avoid repeating identical lambda boilerplate for each picker + auto makeColorSetter = [](int id) -> ValueSetCallback { + return [id](const std::any &v){ CFG_setColor(id, std::any_cast(v)); }; + }; + auto makeColorOpener = [](ColorPickerMenu *picker, int id, std::string name) -> MenuListCallback { + return [picker, id, name](AbstractMenuItem &item) -> InputReactionHint { + picker->reset(CFG_getColor(id), buildColorPresets(id), name); + return DeferToSubmenu(item); + }; + }; + auto makeColorGetter = [](int id) -> ValueGetCallback { + return [id]() -> std::any { return CFG_getColor(id); }; + }; + auto makeColorResetter = [](int id, uint32_t defaultColor) -> ValueResetCallback { + return [id, defaultColor]() { CFG_setColor(id, defaultColor); }; + }; + + // Pre-create one RGB picker per color setting (reused across opens) + std::vector> pickers; + pickers.reserve(std::size(g_colorDefs)); + for (const auto &def : g_colorDefs) + pickers.push_back(std::make_unique( + CFG_getColor(def.id), makeColorSetter(def.id), buildColorPresets(def.id), def.name)); + + // Build color MenuItems (loop order = g_colorDefs order = display order) + std::vector colorMenuItems; + colorMenuItems.reserve(std::size(g_colorDefs)); + for (int i = 0; i < (int)std::size(g_colorDefs); i++) + { + const auto &def = g_colorDefs[i]; + ColorPickerMenu *picker = pickers[i].get(); + colorMenuItems.push_back(new MenuItem{ + ListItemType::Color, def.name, def.desc, colors, color_strings, + makeColorGetter(def.id), + makeColorSetter(def.id), + makeColorResetter(def.id, def.defaultColor), + makeColorOpener(picker, def.id, def.name), picker}); + } + + std::vector appearanceItems; + appearanceItems.push_back(new MenuItem{ListItemType::Generic, "Font", "The font to render all UI text.", {0, 1}, font_names, + []() -> std::any{ return CFG_getFontId(); }, + [](const std::any &value){ CFG_setFontId(std::any_cast(value)); }, + []() { CFG_setFontId(CFG_DEFAULT_FONT_ID);}}); + 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, + []() -> std::any { return CFG_getShowBatteryPercent(); }, + [](const std::any &value) { CFG_setShowBatteryPercent(std::any_cast(value)); }, + []() { CFG_setShowBatteryPercent(CFG_DEFAULT_SHOWBATTERYPERCENT);}}); + appearanceItems.push_back(new MenuItem{ListItemType::Generic, "Show menu animations", "Enable or disable menu animations", {false, true}, on_off, + []() -> std::any{ return CFG_getMenuAnimations(); }, + [](const std::any &value) { CFG_setMenuAnimations(std::any_cast(value)); }, + []() { CFG_setMenuAnimations(CFG_DEFAULT_SHOWMENUANIMATIONS);}}); + appearanceItems.push_back(new MenuItem{ListItemType::Generic, "Show menu transitions", "Enable or disable animated transitions", {false, true}, on_off, + []() -> std::any{ return CFG_getMenuTransitions(); }, + [](const std::any &value) { CFG_setMenuTransitions(std::any_cast(value)); }, + []() { CFG_setMenuTransitions(CFG_DEFAULT_SHOWMENUTRANSITIONS);}}); + appearanceItems.push_back(new MenuItem{ListItemType::Generic, "Game art corner radius", "Set the radius for the rounded corners of game art", 0, 24, "px", + []() -> std::any{ return CFG_getThumbnailRadius(); }, + [](const std::any &value) { CFG_setThumbnailRadius(std::any_cast(value)); }, + []() { CFG_setThumbnailRadius(CFG_DEFAULT_THUMBRADIUS);}}); + appearanceItems.push_back(new MenuItem{ListItemType::Generic, "Game art width", "Set the percentage of screen width used for game art.\nUI elements might overrule this to avoid clipping.", + 5, 100, "%", + []() -> std::any{ return (int)(CFG_getGameArtWidth() * 100); }, + [](const std::any &value) { CFG_setGameArtWidth((double)std::any_cast(value) / 100.0); }, + []() { CFG_setGameArtWidth(CFG_DEFAULT_GAMEARTWIDTH);}}); + appearanceItems.push_back(new MenuItem{ListItemType::Generic, "Show folder names at root", "Show folder names at root directory", {false, true}, on_off, + []() -> std::any { return CFG_getShowFolderNamesAtRoot(); }, + [](const std::any &value) { CFG_setShowFolderNamesAtRoot(std::any_cast(value)); }, + []() { CFG_setShowFolderNamesAtRoot(CFG_DEFAULT_SHOWFOLDERNAMESATROOT);}}); + appearanceItems.push_back(new MenuItem{ListItemType::Generic, "Show Recents", "Show \"Recently Played\" menu entry in game list.", {false, true}, on_off, + []() -> std::any { return CFG_getShowRecents(); }, + [](const std::any &value) { CFG_setShowRecents(std::any_cast(value)); }, + []() { CFG_setShowRecents(CFG_DEFAULT_SHOWRECENTS);}}); + appearanceItems.push_back(new MenuItem{ListItemType::Generic, "Show Tools", "Show \"Tools\" menu entry in game list.", {false, true}, on_off, + []() -> std::any { return CFG_getShowTools(); }, + [](const std::any &value) { CFG_setShowTools(std::any_cast(value)); }, + []() { CFG_setShowTools(CFG_DEFAULT_SHOWTOOLS);}}); + appearanceItems.push_back(new MenuItem{ListItemType::Generic, "Show game art", "Show game artwork in the main menu", {false, true}, on_off, + []() -> std::any { return CFG_getShowGameArt(); }, + [](const std::any &value) { CFG_setShowGameArt(std::any_cast(value)); }, + []() { CFG_setShowGameArt(CFG_DEFAULT_SHOWGAMEART);}}); + appearanceItems.push_back(new MenuItem{ListItemType::Generic, "Use folder background for ROMs", "If enabled, used the emulator background image. Otherwise uses the default.", {false, true}, on_off, + []() -> std::any { return CFG_getRomsUseFolderBackground(); }, + [](const std::any &value) { CFG_setRomsUseFolderBackground(std::any_cast(value)); }, + []() { CFG_setRomsUseFolderBackground(CFG_DEFAULT_ROMSUSEFOLDERBACKGROUND);}}); + appearanceItems.push_back(new MenuItem{ListItemType::Generic, "Show Quickswitcher UI", "Show/hide Quickswitcher UI elements.\nWhen hidden, will only draw background images.", {false, true}, on_off, + []() -> std::any{ return CFG_getShowQuickswitcherUI(); }, + [](const std::any &value){ CFG_setShowQuickswitcherUI(std::any_cast(value)); }, + []() { CFG_setShowQuickswitcherUI(CFG_DEFAULT_SHOWQUICKWITCHERUI);}}); + // not needed anymore + // new MenuItem{ListItemType::Generic, "Game switcher scaling", "The scaling algorithm used to display the savegame image.", scaling, scaling_strings, []() -> std::any + // { return CFG_getGameSwitcherScaling(); }, + // [](const std::any &value) + // { CFG_setGameSwitcherScaling(std::any_cast(value)); }, + // []() { CFG_setGameSwitcherScaling(CFG_DEFAULT_GAMESWITCHERSCALING);}}, + appearanceItems.push_back(new MenuItem{ListItemType::Button, "Reset to defaults", "Resets all options in this menu to their default values.", ResetCurrentMenu}); + auto *appearanceMenu = new MenuList(MenuItemType::Fixed, "Appearance", std::move(appearanceItems)); std::vector displayItems = { new MenuItem{ListItemType::Generic, "Brightness", "Display brightness (0 to 10)", 0, 10, "",[]() -> std::any @@ -912,6 +942,8 @@ int main(int argc, char *argv[]) delete systemMenu; ctx.menu = NULL; + // Color pickers are owned by unique_ptrs above; destroyed automatically here. + QuitSettings(); PWR_quit(); PAD_quit();