From ed5ca0dc618b047684cb0da472fe083470b94472 Mon Sep 17 00:00:00 2001 From: Tom Singleton Date: Tue, 24 Feb 2026 16:30:03 +0000 Subject: [PATCH 01/10] Basic BG color picking when no bg image present --- workspace/all/nextui/nextui.c | 39 +++++++++++++++++++++++------ workspace/all/settings/settings.cpp | 11 +++++--- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/workspace/all/nextui/nextui.c b/workspace/all/nextui/nextui.c index c2aaefd3e..99bd974a7 100644 --- a/workspace/all/nextui/nextui.c +++ b/workspace/all/nextui/nextui.c @@ -3131,7 +3131,17 @@ int main (int argc, char *argv[]) { if(animationdirection != ANIM_NONE) { if(CFG_getMenuTransitions()) { - GFX_clearLayers(LAYER_BACKGROUND); + SDL_LockMutex(bgMutex); + if(folderbgbmp) { + GFX_drawOnLayer(folderbgbmp, 0, 0, screen->w, screen->h, 1.0f, 0, LAYER_BACKGROUND); + } else { + SDL_Surface *bgfill = SDL_CreateRGBSurfaceWithFormat(0, screen->w, screen->h, screen->format->BitsPerPixel, screen->format->format); + uint32_t bgc = CFG_getColor(7); + SDL_FillRect(bgfill, NULL, SDL_MapRGB(screen->format, (bgc >> 16) & 0xFF, (bgc >> 8) & 0xFF, bgc & 0xFF)); + GFX_drawOnLayer(bgfill, 0, 0, screen->w, screen->h, 1.0f, 0, LAYER_BACKGROUND); + SDL_FreeSurface(bgfill); + } + SDL_UnlockMutex(bgMutex); folderbgchanged = 1; GFX_clearLayers(LAYER_TRANSITION); GFX_flipHidden(); @@ -3152,8 +3162,13 @@ int main (int argc, char *argv[]) { if(folderbgchanged) { if(folderbgbmp) GFX_drawOnLayer(folderbgbmp,0, 0, screen->w, screen->h,1.0f,0,LAYER_BACKGROUND); - else - GFX_clearLayers(LAYER_BACKGROUND); + else { + SDL_Surface *bgfill = SDL_CreateRGBSurfaceWithFormat(0, screen->w, screen->h, screen->format->BitsPerPixel, screen->format->format); + uint32_t bgc = CFG_getColor(7); + SDL_FillRect(bgfill, NULL, SDL_MapRGB(screen->format, (bgc >> 16) & 0xFF, (bgc >> 8) & 0xFF, bgc & 0xFF)); + GFX_drawOnLayer(bgfill, 0, 0, screen->w, screen->h, 1.0f, 0, LAYER_BACKGROUND); + SDL_FreeSurface(bgfill); + } folderbgchanged = 0; } SDL_UnlockMutex(bgMutex); @@ -3163,8 +3178,13 @@ int main (int argc, char *argv[]) { if(folderbgchanged) { if(folderbgbmp) GFX_drawOnLayer(folderbgbmp,0, 0, screen->w, screen->h,1.0f,0,LAYER_BACKGROUND); - else - GFX_clearLayers(LAYER_BACKGROUND); + else { + SDL_Surface *bgfill = SDL_CreateRGBSurfaceWithFormat(0, screen->w, screen->h, screen->format->BitsPerPixel, screen->format->format); + uint32_t bgc = CFG_getColor(7); + SDL_FillRect(bgfill, NULL, SDL_MapRGB(screen->format, (bgc >> 16) & 0xFF, (bgc >> 8) & 0xFF, bgc & 0xFF)); + GFX_drawOnLayer(bgfill, 0, 0, screen->w, screen->h, 1.0f, 0, LAYER_BACKGROUND); + SDL_FreeSurface(bgfill); + } folderbgchanged = 0; } SDL_UnlockMutex(bgMutex); @@ -3216,8 +3236,13 @@ int main (int argc, char *argv[]) { if(folderbgchanged) { if(folderbgbmp) GFX_drawOnLayer(folderbgbmp,0, 0, screen->w, screen->h,1.0f,0,LAYER_BACKGROUND); - else - GFX_clearLayers(LAYER_BACKGROUND); + else { + SDL_Surface *bgfill = SDL_CreateRGBSurfaceWithFormat(0, screen->w, screen->h, screen->format->BitsPerPixel, screen->format->format); + uint32_t bgc = CFG_getColor(7); + SDL_FillRect(bgfill, NULL, SDL_MapRGB(screen->format, (bgc >> 16) & 0xFF, (bgc >> 8) & 0xFF, bgc & 0xFF)); + GFX_drawOnLayer(bgfill, 0, 0, screen->w, screen->h, 1.0f, 0, LAYER_BACKGROUND); + SDL_FreeSurface(bgfill); + } folderbgchanged = 0; } SDL_UnlockMutex(bgMutex); diff --git a/workspace/all/settings/settings.cpp b/workspace/all/settings/settings.cpp index 8a8f2a9c4..f208e9d78 100644 --- a/workspace/all/settings/settings.cpp +++ b/workspace/all/settings/settings.cpp @@ -178,10 +178,10 @@ int main(int argc, char *argv[]) []() -> std::any { return CFG_getColor(5); }, [](const std::any &value) { CFG_setColor(5, std::any_cast(value)); }, []() { CFG_setColor(5, CFG_DEFAULT_COLOR5);}}, - //new MenuItem{ListItemType::Color, "Background color", "Main UI background color", colors, color_strings, - //[]() -> std::any { return CFG_getColor(7); }, - //[](const std::any &value) { CFG_setColor(7, std::any_cast(value)); }, - //[]() { CFG_setColor(7, CFG_DEFAULT_COLOR7);}}, + new MenuItem{ListItemType::Color, "Background Color", "Background color used when no background image is set.", colors, color_strings, + []() -> std::any { return CFG_getColor(7); }, + [](const std::any &value) { CFG_setColor(7, std::any_cast(value)); }, + []() { CFG_setColor(7, 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)); }, @@ -574,6 +574,9 @@ int main(int argc, char *argv[]) if(bgbmp) { SDL_Rect image_rect = {0, 0, ctx.screen->w, ctx.screen->h}; SDL_BlitSurface(bgbmp, NULL, ctx.screen, &image_rect); + } else { + uint32_t bgc = CFG_getColor(7); + SDL_FillRect(ctx.screen, NULL, SDL_MapRGB(ctx.screen->format, (bgc >> 16) & 0xFF, (bgc >> 8) & 0xFF, bgc & 0xFF)); } int ow = 0; From 94fa11151cfca373aec2bef0bd20b7de7c7ebd22 Mon Sep 17 00:00:00 2001 From: Tom Singleton Date: Wed, 25 Feb 2026 06:33:47 +0000 Subject: [PATCH 02/10] Color picker for apperance options --- workspace/all/settings/menu.cpp | 247 +++++++++++++++++++++++++++- workspace/all/settings/menu.hpp | 29 ++++ workspace/all/settings/settings.cpp | 104 +++++++++--- 3 files changed, 359 insertions(+), 21 deletions(-) diff --git a/workspace/all/settings/menu.cpp b/workspace/all/settings/menu.cpp index 5566e3912..a694f5f39 100644 --- a/workspace/all/settings/menu.cpp +++ b/workspace/all/settings/menu.cpp @@ -691,7 +691,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, @@ -702,6 +708,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) { @@ -889,6 +900,240 @@ bool MenuList::isOverlayVisible() return overlayVisible; } +/////////////////////////////////////////////////////////// +// ColorPickerMenu + +ColorPickerMenu::ColorPickerMenu(uint32_t initialColor, ValueSetCallback on_set, + std::vector presets) + : MenuList(MenuItemType::Custom, "", {}), on_set(on_set), + presets(std::move(presets)), r(0), g(0), b(0), selected(0) +{ + r = (initialColor >> 16) & 0xFF; + g = (initialColor >> 8) & 0xFF; + b = initialColor & 0xFF; +} + +void ColorPickerMenu::reset(uint32_t color, std::vector newPresets) +{ + r = (color >> 16) & 0xFF; + g = (color >> 8) & 0xFF; + b = color & 0xFF; + selected = 0; + presets = std::move(newPresets); +} + +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 = 3 + (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)) + { + quit = 1; + return NoOp; + } + + if (selected < 3) + { + int *channel = (selected == 0) ? &r : (selected == 1) ? &g : &b; + 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 - 3; + if (PAD_justPressed(BTN_A) && presetIdx < (int)presets.size()) + { + const auto &preset = presets[presetIdx]; + r = (preset.color >> 16) & 0xFF; + g = (preset.color >> 8) & 0xFF; + b = preset.color & 0xFF; + applyColor(); + selected = 0; + dirty = 1; + return NoOp; + } + } + + return Unhandled; +} + +void ColorPickerMenu::drawSlider(SDL_Surface *surface, const SDL_Rect &row, + const char *label, int value, bool is_selected, int channel) +{ + if (is_selected) + GFX_blitPillLightCPP(ASSET_BUTTON, surface, row); + + SDL_Color text_color = is_selected ? uintToColour(THEME_COLOR5_255) : uintToColour(THEME_COLOR4_255); + + // Channel label ("R", "G", "B") + SDL_Surface *label_surf = TTF_RenderUTF8_Blended(font.small, label, text_color); + int label_w = label_surf->w; + SDL_BlitSurfaceCPP(label_surf, {}, surface, + {row.x + SCALE1(OPTION_PADDING), row.y + (row.h - label_surf->h) / 2}); + SDL_FreeSurface(label_surf); + + // Hex value right-aligned ("FF", "00", etc.) + 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_w = hex_surf->w; + SDL_BlitSurfaceCPP(hex_surf, {}, surface, + {row.x + row.w - hex_w - SCALE1(OPTION_PADDING), + row.y + (row.h - hex_surf->h) / 2}); + SDL_FreeSurface(hex_surf); + + // Slider bar + int bar_x = row.x + SCALE1(OPTION_PADDING) + label_w + SCALE1(OPTION_PADDING); + int bar_w = row.w - SCALE1(OPTION_PADDING) - label_w - SCALE1(OPTION_PADDING * 3) - hex_w; + int bar_h = SCALE1(6); + int bar_y = row.y + (row.h - bar_h) / 2; + + if (bar_w > 0) + { + SDL_Rect bg = {bar_x, bar_y, bar_w, bar_h}; + SDL_FillRect(surface, &bg, SDL_MapRGB(surface->format, 50, 50, 50)); + + int fill_w = (int)(bar_w * value / 255.0f); + if (fill_w > 0) + { + SDL_Rect fill = {bar_x, bar_y, fill_w, bar_h}; + uint8_t cr = (channel == 0) ? 255 : 0; + uint8_t cg = (channel == 1) ? 255 : 0; + uint8_t cb = (channel == 2) ? 255 : 0; + SDL_FillRect(surface, &fill, SDL_MapRGB(surface->format, cr, cg, cb)); + } + } +} + +void ColorPickerMenu::drawPreset(SDL_Surface *surface, const SDL_Rect &row, + const ColorPreset &preset, bool is_selected) +{ + if (is_selected) + GFX_blitPillLightCPP(ASSET_BUTTON, surface, row); + + // 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) +{ + const int PREVIEW_W = SCALE1(BUTTON_SIZE * 2 + PADDING); + const int SLIDER_AREA_W = dst.w - PREVIEW_W - SCALE1(PADDING); + + // R, G, B slider rows + const char *channel_labels[] = {"R", "G", "B"}; + int channel_values[] = {r, g, b}; + for (int i = 0; i < 3; i++) + { + SDL_Rect row = {dst.x, dst.y + 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 + SDL_Rect preview = { + dst.x + SLIDER_AREA_W + SCALE1(PADDING), + dst.y, + PREVIEW_W, + SCALE1(BUTTON_SIZE * 3) + }; + SDL_FillRect(surface, &preview, SDL_MapRGB(surface->format, 255, 255, 255)); + SDL_Rect preview_inner = {preview.x + 1, preview.y + 1, preview.w - 2, preview.h - 2}; + uint32_t col = currentColor(); + SDL_FillRect(surface, &preview_inner, + SDL_MapRGB(surface->format, (col >> 16) & 0xFF, (col >> 8) & 0xFF, col & 0xFF)); + + // Preset rows below the sliders + for (int i = 0; i < (int)presets.size(); i++) + { + SDL_Rect row = { + dst.x, + dst.y + SCALE1(BUTTON_SIZE * 3) + SCALE1(i * BUTTON_SIZE), + dst.w, + SCALE1(BUTTON_SIZE) + }; + drawPreset(surface, row, presets[i], selected == 3 + i); + } +} + 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 4ca964d60..1b0b74cec 100644 --- a/workspace/all/settings/menu.hpp +++ b/workspace/all/settings/menu.hpp @@ -342,4 +342,33 @@ const MenuListCallback DeferToSubmenu = [](AbstractMenuItem &itm) -> InputReacti const MenuListCallback ResetCurrentMenu = [](AbstractMenuItem &itm) -> InputReactionHint { return InputReactionHint::ResetAllItems; +}; + +struct ColorPreset { + uint32_t color; // 0xRRGGBB + std::string label; +}; + +class ColorPickerMenu : public MenuList +{ + int r, g, b; // 0–255 each + int selected; // 0=R, 1=G, 2=B, 3+N=presets + ValueSetCallback on_set; + std::vector presets; + + 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); + + void reset(uint32_t color, std::vector newPresets); + + InputReactionHint handleInput(int &dirty, int &quit) override; + void drawCustom(SDL_Surface *surface, const SDL_Rect &dst) override; }; \ No newline at end of file diff --git a/workspace/all/settings/settings.cpp b/workspace/all/settings/settings.cpp index f208e9d78..808493d51 100644 --- a/workspace/all/settings/settings.cpp +++ b/workspace/all/settings/settings.cpp @@ -82,6 +82,28 @@ static const std::vector scaling_strings = {"Fullscreen", "Fit", "F static const std::vector scaling = {(int)GFX_SCALE_FULLSCREEN, (int)GFX_SCALE_FIT, (int)GFX_SCALE_FILL}; namespace { + struct ColorDef { int id; const char *name; }; + static const ColorDef g_colorDefs[] = { + {1, "Main Color"}, + {2, "Primary Accent Color"}, + {3, "Secondary Accent Color"}, + {4, "List Text"}, + {5, "List Text Selected"}, + {6, "Hint info Color"}, + {7, "Background Color"}, + }; + + 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; @@ -149,39 +171,76 @@ int main(int argc, char *argv[]) tz_labels.push_back(std::string(timezones[i])); } + // Pre-create one RGB picker per color setting (reused across opens) + auto *picker1 = new ColorPickerMenu(CFG_getColor(1), [](const std::any &v){ CFG_setColor(1, std::any_cast(v)); }, buildColorPresets(1)); + auto *picker2 = new ColorPickerMenu(CFG_getColor(2), [](const std::any &v){ CFG_setColor(2, std::any_cast(v)); }, buildColorPresets(2)); + auto *picker3 = new ColorPickerMenu(CFG_getColor(3), [](const std::any &v){ CFG_setColor(3, std::any_cast(v)); }, buildColorPresets(3)); + auto *picker4 = new ColorPickerMenu(CFG_getColor(4), [](const std::any &v){ CFG_setColor(4, std::any_cast(v)); }, buildColorPresets(4)); + auto *picker5 = new ColorPickerMenu(CFG_getColor(5), [](const std::any &v){ CFG_setColor(5, std::any_cast(v)); }, buildColorPresets(5)); + auto *picker6 = new ColorPickerMenu(CFG_getColor(6), [](const std::any &v){ CFG_setColor(6, std::any_cast(v)); }, buildColorPresets(6)); + auto *picker7 = new ColorPickerMenu(CFG_getColor(7), [](const std::any &v){ CFG_setColor(7, std::any_cast(v)); }, buildColorPresets(7)); + auto appearanceMenu = new MenuList(MenuItemType::Fixed, "Appearance", - {new MenuItem{ListItemType::Generic, "Font", "The font to render all UI text.", {0, 1}, font_names, + {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(1); }, + new MenuItem{ListItemType::Color, "Main Color", "The color used to render main UI elements.", colors, color_strings, + []() -> std::any{ return CFG_getColor(1); }, [](const std::any &value){ CFG_setColor(1, std::any_cast(value)); }, - []() { CFG_setColor(1, 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(2); }, + []() { CFG_setColor(1, CFG_DEFAULT_COLOR1);}, + [picker1](AbstractMenuItem &item) -> InputReactionHint { + picker1->reset(CFG_getColor(1), buildColorPresets(1)); + return DeferToSubmenu(item); + }, picker1}, + 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(2); }, [](const std::any &value){ CFG_setColor(2, std::any_cast(value)); }, - []() { CFG_setColor(2, CFG_DEFAULT_COLOR2);}}, - new MenuItem{ListItemType::Color, "Secondary Accent Color", "A secondary highlight color.", colors, color_strings, - []() -> std::any{ return CFG_getColor(3); }, + []() { CFG_setColor(2, CFG_DEFAULT_COLOR2);}, + [picker2](AbstractMenuItem &item) -> InputReactionHint { + picker2->reset(CFG_getColor(2), buildColorPresets(2)); + return DeferToSubmenu(item); + }, picker2}, + new MenuItem{ListItemType::Color, "Secondary Accent Color", "A secondary highlight color.", colors, color_strings, + []() -> std::any{ return CFG_getColor(3); }, [](const std::any &value){ CFG_setColor(3, std::any_cast(value)); }, - []() { CFG_setColor(3, CFG_DEFAULT_COLOR3);}}, - new MenuItem{ListItemType::Color, "Hint info Color", "Color for button hints and info", colors, color_strings, - []() -> std::any{ return CFG_getColor(6); }, + []() { CFG_setColor(3, CFG_DEFAULT_COLOR3);}, + [picker3](AbstractMenuItem &item) -> InputReactionHint { + picker3->reset(CFG_getColor(3), buildColorPresets(3)); + return DeferToSubmenu(item); + }, picker3}, + new MenuItem{ListItemType::Color, "Hint info Color", "Color for button hints and info", colors, color_strings, + []() -> std::any{ return CFG_getColor(6); }, [](const std::any &value){ CFG_setColor(6, std::any_cast(value)); }, - []() { CFG_setColor(6, CFG_DEFAULT_COLOR6);}}, - new MenuItem{ListItemType::Color, "List Text", "List text color", colors, color_strings, - []() -> std::any{ return CFG_getColor(4); }, + []() { CFG_setColor(6, CFG_DEFAULT_COLOR6);}, + [picker6](AbstractMenuItem &item) -> InputReactionHint { + picker6->reset(CFG_getColor(6), buildColorPresets(6)); + return DeferToSubmenu(item); + }, picker6}, + new MenuItem{ListItemType::Color, "List Text", "List text color", colors, color_strings, + []() -> std::any{ return CFG_getColor(4); }, [](const std::any &value){ CFG_setColor(4, std::any_cast(value)); }, - []() { CFG_setColor(4, CFG_DEFAULT_COLOR4);}}, - new MenuItem{ListItemType::Color, "List Text Selected", "List selected text color", colors, color_strings, - []() -> std::any { return CFG_getColor(5); }, + []() { CFG_setColor(4, CFG_DEFAULT_COLOR4);}, + [picker4](AbstractMenuItem &item) -> InputReactionHint { + picker4->reset(CFG_getColor(4), buildColorPresets(4)); + return DeferToSubmenu(item); + }, picker4}, + new MenuItem{ListItemType::Color, "List Text Selected", "List selected text color", colors, color_strings, + []() -> std::any { return CFG_getColor(5); }, [](const std::any &value) { CFG_setColor(5, std::any_cast(value)); }, - []() { CFG_setColor(5, CFG_DEFAULT_COLOR5);}}, + []() { CFG_setColor(5, CFG_DEFAULT_COLOR5);}, + [picker5](AbstractMenuItem &item) -> InputReactionHint { + picker5->reset(CFG_getColor(5), buildColorPresets(5)); + return DeferToSubmenu(item); + }, picker5}, new MenuItem{ListItemType::Color, "Background Color", "Background color used when no background image is set.", colors, color_strings, []() -> std::any { return CFG_getColor(7); }, [](const std::any &value) { CFG_setColor(7, std::any_cast(value)); }, - []() { CFG_setColor(7, CFG_DEFAULT_COLOR7);}}, + []() { CFG_setColor(7, CFG_DEFAULT_COLOR7);}, + [picker7](AbstractMenuItem &item) -> InputReactionHint { + picker7->reset(CFG_getColor(7), buildColorPresets(7)); + return DeferToSubmenu(item); + }, picker7}, 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)); }, @@ -634,6 +693,11 @@ int main(int argc, char *argv[]) delete systemMenu; ctx.menu = NULL; + // Color pickers are not owned by their menu items (submenu deletion is + // commented out in AbstractMenuItem), so delete them explicitly. + delete picker1; delete picker2; delete picker3; + delete picker4; delete picker5; delete picker6; delete picker7; + QuitSettings(); PWR_quit(); PAD_quit(); From fc40664a182d91ad8774a32317b144061bd053a3 Mon Sep 17 00:00:00 2001 From: Tom Singleton Date: Wed, 25 Feb 2026 07:25:27 +0000 Subject: [PATCH 03/10] Padding and border for color square --- workspace/all/settings/menu.cpp | 66 ++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/workspace/all/settings/menu.cpp b/workspace/all/settings/menu.cpp index a694f5f39..57a5370d8 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; @@ -1094,39 +1095,78 @@ void ColorPickerMenu::drawPreset(SDL_Surface *surface, const SDL_Rect &row, SDL_FreeSurface(name_surf); } +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); + } +} + void ColorPickerMenu::drawCustom(SDL_Surface *surface, const SDL_Rect &dst) { - const int PREVIEW_W = SCALE1(BUTTON_SIZE * 2 + PADDING); - const int SLIDER_AREA_W = dst.w - PREVIEW_W - SCALE1(PADDING); + 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 = currentColor(); + uint32_t col_mapped = SDL_MapRGB(surface->format, (col >> 16) & 0xFF, (col >> 8) & 0xFF, col & 0xFF); // R, G, B slider rows const char *channel_labels[] = {"R", "G", "B"}; int channel_values[] = {r, g, b}; for (int i = 0; i < 3; i++) { - SDL_Rect row = {dst.x, dst.y + SCALE1(i * BUTTON_SIZE), SLIDER_AREA_W, SCALE1(BUTTON_SIZE)}; + 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 + // Color preview square to the right of sliders, centered vertically in the 3-slider area SDL_Rect preview = { dst.x + SLIDER_AREA_W + SCALE1(PADDING), - dst.y, - PREVIEW_W, - SCALE1(BUTTON_SIZE * 3) + dst.y + TOP_OFFSET + (SCALE1(BUTTON_SIZE * 3) - PREVIEW_SIZE) / 2, + PREVIEW_SIZE, + PREVIEW_SIZE }; - SDL_FillRect(surface, &preview, SDL_MapRGB(surface->format, 255, 255, 255)); - SDL_Rect preview_inner = {preview.x + 1, preview.y + 1, preview.w - 2, preview.h - 2}; - uint32_t col = currentColor(); - SDL_FillRect(surface, &preview_inner, - SDL_MapRGB(surface->format, (col >> 16) & 0xFF, (col >> 8) & 0xFF, col & 0xFF)); + drawRoundedRect(surface, preview, PREVIEW_RADIUS, white, black, col_mapped); // Preset rows below the sliders for (int i = 0; i < (int)presets.size(); i++) { SDL_Rect row = { dst.x, - dst.y + SCALE1(BUTTON_SIZE * 3) + SCALE1(i * BUTTON_SIZE), + dst.y + TOP_OFFSET + SCALE1(BUTTON_SIZE * 3) + SCALE1(i * BUTTON_SIZE), dst.w, SCALE1(BUTTON_SIZE) }; From 27b63aa077f9cfd2dcbb91097bf7813675bb1ad7 Mon Sep 17 00:00:00 2001 From: Tom Singleton Date: Wed, 25 Feb 2026 07:58:33 +0000 Subject: [PATCH 04/10] Slider style improvements --- workspace/all/settings/menu.cpp | 138 +++++++++++++++++++++----------- 1 file changed, 90 insertions(+), 48 deletions(-) diff --git a/workspace/all/settings/menu.cpp b/workspace/all/settings/menu.cpp index 57a5370d8..dccacf6a1 100644 --- a/workspace/all/settings/menu.cpp +++ b/workspace/all/settings/menu.cpp @@ -1013,6 +1013,77 @@ InputReactionHint ColorPickerMenu::handleInput(int &dirty, int &quit) 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) { @@ -1046,18 +1117,25 @@ void ColorPickerMenu::drawSlider(SDL_Surface *surface, const SDL_Rect &row, if (bar_w > 0) { - SDL_Rect bg = {bar_x, bar_y, bar_w, bar_h}; - SDL_FillRect(surface, &bg, SDL_MapRGB(surface->format, 50, 50, 50)); - - int fill_w = (int)(bar_w * value / 255.0f); - if (fill_w > 0) - { - SDL_Rect fill = {bar_x, bar_y, fill_w, bar_h}; - uint8_t cr = (channel == 0) ? 255 : 0; - uint8_t cg = (channel == 1) ? 255 : 0; - uint8_t cb = (channel == 2) ? 255 : 0; - SDL_FillRect(surface, &fill, SDL_MapRGB(surface->format, cr, cg, cb)); - } + // Gradient capsule + drawGradientCapsule(surface, {bar_x, bar_y, bar_w, bar_h}, this->r, this->g, this->b, channel); + + // Circle thumb indicator + const int circ_d = bar_h + SCALE1(4); // slightly larger than bar + const int circ_border = SCALE1(2); // black ring thickness (each side) + uint32_t white = SDL_MapRGB(surface->format, 255, 255, 255); + uint32_t black = SDL_MapRGB(surface->format, 0, 0, 0); + + int cx = bar_x + (int)((float)value / 255.0f * (bar_w - 1)); + int cy = bar_y + bar_h / 2; + // clamp so circle stays inside bar bounds + cx = std::max(bar_x + circ_d / 2, std::min(bar_x + bar_w - 1 - circ_d / 2, cx)); + + SDL_Rect outer_rect = {cx - circ_d / 2, cy - circ_d / 2, circ_d, circ_d}; + drawSolidRoundedRect(surface, outer_rect, circ_d / 2, black); + int inner_d = circ_d - circ_border * 2; + SDL_Rect inner_rect = {cx - inner_d / 2, cy - inner_d / 2, inner_d, inner_d}; + drawSolidRoundedRect(surface, inner_rect, inner_d / 2, white); } } @@ -1095,42 +1173,6 @@ void ColorPickerMenu::drawPreset(SDL_Surface *surface, const SDL_Rect &row, SDL_FreeSurface(name_surf); } -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); - } -} - void ColorPickerMenu::drawCustom(SDL_Surface *surface, const SDL_Rect &dst) { const int TOP_OFFSET = SCALE1(4); From f454ef2e50590b364fbbe527267f8b4b005b0661 Mon Sep 17 00:00:00 2001 From: Tom Singleton Date: Wed, 25 Feb 2026 08:28:59 +0000 Subject: [PATCH 05/10] Slider black and white border --- workspace/all/settings/menu.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/workspace/all/settings/menu.cpp b/workspace/all/settings/menu.cpp index dccacf6a1..95a6ac5d8 100644 --- a/workspace/all/settings/menu.cpp +++ b/workspace/all/settings/menu.cpp @@ -1117,14 +1117,21 @@ void ColorPickerMenu::drawSlider(SDL_Surface *surface, const SDL_Rect &row, if (bar_w > 0) { + uint32_t white = SDL_MapRGB(surface->format, 255, 255, 255); + uint32_t black = SDL_MapRGB(surface->format, 0, 0, 0); + + // Capsule border: black outer ring, white inner ring (mirrors color preview style) + SDL_Rect border_outer = {bar_x - 2, bar_y - 2, bar_w + 4, bar_h + 4}; + drawSolidRoundedRect(surface, border_outer, bar_h / 2 + 2, black); + SDL_Rect border_inner = {bar_x - 1, bar_y - 1, bar_w + 2, bar_h + 2}; + drawSolidRoundedRect(surface, border_inner, bar_h / 2 + 1, white); + // Gradient capsule drawGradientCapsule(surface, {bar_x, bar_y, bar_w, bar_h}, this->r, this->g, this->b, channel); // Circle thumb indicator const int circ_d = bar_h + SCALE1(4); // slightly larger than bar const int circ_border = SCALE1(2); // black ring thickness (each side) - uint32_t white = SDL_MapRGB(surface->format, 255, 255, 255); - uint32_t black = SDL_MapRGB(surface->format, 0, 0, 0); int cx = bar_x + (int)((float)value / 255.0f * (bar_w - 1)); int cy = bar_y + bar_h / 2; From 5476eb1911b6fd967c943a92e4730e738f2bdc3f Mon Sep 17 00:00:00 2001 From: Tom Singleton Date: Thu, 26 Feb 2026 08:39:48 +0000 Subject: [PATCH 06/10] Separate color picker from menu.cpp --- workspace/all/settings/colorpickermenu.cpp | 325 +++++++++++++++++++++ workspace/all/settings/colorpickermenu.hpp | 32 ++ workspace/all/settings/makefile | 2 +- workspace/all/settings/menu.cpp | 321 -------------------- workspace/all/settings/menu.hpp | 28 -- workspace/all/settings/settings.cpp | 1 + 6 files changed, 359 insertions(+), 350 deletions(-) create mode 100644 workspace/all/settings/colorpickermenu.cpp create mode 100644 workspace/all/settings/colorpickermenu.hpp diff --git a/workspace/all/settings/colorpickermenu.cpp b/workspace/all/settings/colorpickermenu.cpp new file mode 100644 index 000000000..fd905d359 --- /dev/null +++ b/workspace/all/settings/colorpickermenu.cpp @@ -0,0 +1,325 @@ +#include "colorpickermenu.hpp" + +#include + +/////////////////////////////////////////////////////////// +// ColorPickerMenu + +ColorPickerMenu::ColorPickerMenu(uint32_t initialColor, ValueSetCallback on_set, + std::vector presets) + : MenuList(MenuItemType::Custom, "", {}), on_set(on_set), + presets(std::move(presets)), r(0), g(0), b(0), selected(0) +{ + r = (initialColor >> 16) & 0xFF; + g = (initialColor >> 8) & 0xFF; + b = initialColor & 0xFF; +} + +void ColorPickerMenu::reset(uint32_t color, std::vector newPresets) +{ + r = (color >> 16) & 0xFF; + g = (color >> 8) & 0xFF; + b = color & 0xFF; + selected = 0; + presets = std::move(newPresets); +} + +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 = 3 + (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)) + { + quit = 1; + return NoOp; + } + + if (selected < 3) + { + int *channel = (selected == 0) ? &r : (selected == 1) ? &g : &b; + 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 - 3; + if (PAD_justPressed(BTN_A) && presetIdx < (int)presets.size()) + { + const auto &preset = presets[presetIdx]; + r = (preset.color >> 16) & 0xFF; + g = (preset.color >> 8) & 0xFF; + b = preset.color & 0xFF; + 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) + GFX_blitPillLightCPP(ASSET_BUTTON, surface, row); + + SDL_Color text_color = is_selected ? uintToColour(THEME_COLOR5_255) : uintToColour(THEME_COLOR4_255); + + // Channel label ("R", "G", "B") + SDL_Surface *label_surf = TTF_RenderUTF8_Blended(font.small, label, text_color); + int label_w = label_surf->w; + SDL_BlitSurfaceCPP(label_surf, {}, surface, + {row.x + SCALE1(OPTION_PADDING), row.y + (row.h - label_surf->h) / 2}); + SDL_FreeSurface(label_surf); + + // Hex value right-aligned ("FF", "00", etc.) + 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_w = hex_surf->w; + SDL_BlitSurfaceCPP(hex_surf, {}, surface, + {row.x + row.w - hex_w - SCALE1(OPTION_PADDING), + row.y + (row.h - hex_surf->h) / 2}); + SDL_FreeSurface(hex_surf); + + // Slider bar + int bar_x = row.x + SCALE1(OPTION_PADDING) + label_w + SCALE1(OPTION_PADDING); + int bar_w = row.w - SCALE1(OPTION_PADDING) - label_w - SCALE1(OPTION_PADDING * 3) - hex_w; + int bar_h = SCALE1(6); + 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); + + // Capsule border: black outer ring, white inner ring (mirrors color preview style) + SDL_Rect border_outer = {bar_x - 2, bar_y - 2, bar_w + 4, bar_h + 4}; + drawSolidRoundedRect(surface, border_outer, bar_h / 2 + 2, black); + SDL_Rect border_inner = {bar_x - 1, bar_y - 1, bar_w + 2, bar_h + 2}; + drawSolidRoundedRect(surface, border_inner, bar_h / 2 + 1, white); + + // Gradient capsule + drawGradientCapsule(surface, {bar_x, bar_y, bar_w, bar_h}, this->r, this->g, this->b, channel); + + // Circle thumb indicator + const int circ_d = bar_h + SCALE1(4); // slightly larger than bar + const int circ_border = SCALE1(2); // black ring thickness (each side) + + int cx = bar_x + (int)((float)value / 255.0f * (bar_w - 1)); + int cy = bar_y + bar_h / 2; + // clamp so circle stays inside bar bounds + cx = std::max(bar_x + circ_d / 2, std::min(bar_x + bar_w - 1 - circ_d / 2, cx)); + + SDL_Rect outer_rect = {cx - circ_d / 2, cy - circ_d / 2, circ_d, circ_d}; + drawSolidRoundedRect(surface, outer_rect, circ_d / 2, black); + int inner_d = circ_d - circ_border * 2; + SDL_Rect inner_rect = {cx - inner_d / 2, cy - inner_d / 2, inner_d, inner_d}; + drawSolidRoundedRect(surface, inner_rect, inner_d / 2, white); + } +} + +void ColorPickerMenu::drawPreset(SDL_Surface *surface, const SDL_Rect &row, + const ColorPreset &preset, bool is_selected) +{ + if (is_selected) + GFX_blitPillLightCPP(ASSET_BUTTON, surface, row); + + // 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) +{ + 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 = currentColor(); + uint32_t col_mapped = SDL_MapRGB(surface->format, (col >> 16) & 0xFF, (col >> 8) & 0xFF, col & 0xFF); + + // R, G, B slider rows + const char *channel_labels[] = {"R", "G", "B"}; + int channel_values[] = {r, g, b}; + for (int i = 0; i < 3; 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 3-slider area + SDL_Rect preview = { + dst.x + SLIDER_AREA_W + SCALE1(PADDING), + dst.y + TOP_OFFSET + (SCALE1(BUTTON_SIZE * 3) - PREVIEW_SIZE) / 2, + PREVIEW_SIZE, + PREVIEW_SIZE + }; + drawRoundedRect(surface, preview, PREVIEW_RADIUS, white, black, 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 * 3) + SCALE1(i * BUTTON_SIZE), + dst.w, + SCALE1(BUTTON_SIZE) + }; + drawPreset(surface, row, presets[i], selected == 3 + i); + } +} diff --git a/workspace/all/settings/colorpickermenu.hpp b/workspace/all/settings/colorpickermenu.hpp new file mode 100644 index 000000000..991be481b --- /dev/null +++ b/workspace/all/settings/colorpickermenu.hpp @@ -0,0 +1,32 @@ +#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; + + 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); + + void reset(uint32_t color, std::vector newPresets); + + 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 2341cad30..724ae2c30 100644 --- a/workspace/all/settings/menu.cpp +++ b/workspace/all/settings/menu.cpp @@ -929,327 +929,6 @@ bool MenuList::isOverlayVisible() return overlayVisible; } -/////////////////////////////////////////////////////////// -// ColorPickerMenu - -ColorPickerMenu::ColorPickerMenu(uint32_t initialColor, ValueSetCallback on_set, - std::vector presets) - : MenuList(MenuItemType::Custom, "", {}), on_set(on_set), - presets(std::move(presets)), r(0), g(0), b(0), selected(0) -{ - r = (initialColor >> 16) & 0xFF; - g = (initialColor >> 8) & 0xFF; - b = initialColor & 0xFF; -} - -void ColorPickerMenu::reset(uint32_t color, std::vector newPresets) -{ - r = (color >> 16) & 0xFF; - g = (color >> 8) & 0xFF; - b = color & 0xFF; - selected = 0; - presets = std::move(newPresets); -} - -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 = 3 + (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)) - { - quit = 1; - return NoOp; - } - - if (selected < 3) - { - int *channel = (selected == 0) ? &r : (selected == 1) ? &g : &b; - 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 - 3; - if (PAD_justPressed(BTN_A) && presetIdx < (int)presets.size()) - { - const auto &preset = presets[presetIdx]; - r = (preset.color >> 16) & 0xFF; - g = (preset.color >> 8) & 0xFF; - b = preset.color & 0xFF; - 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) - GFX_blitPillLightCPP(ASSET_BUTTON, surface, row); - - SDL_Color text_color = is_selected ? uintToColour(THEME_COLOR5_255) : uintToColour(THEME_COLOR4_255); - - // Channel label ("R", "G", "B") - SDL_Surface *label_surf = TTF_RenderUTF8_Blended(font.small, label, text_color); - int label_w = label_surf->w; - SDL_BlitSurfaceCPP(label_surf, {}, surface, - {row.x + SCALE1(OPTION_PADDING), row.y + (row.h - label_surf->h) / 2}); - SDL_FreeSurface(label_surf); - - // Hex value right-aligned ("FF", "00", etc.) - 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_w = hex_surf->w; - SDL_BlitSurfaceCPP(hex_surf, {}, surface, - {row.x + row.w - hex_w - SCALE1(OPTION_PADDING), - row.y + (row.h - hex_surf->h) / 2}); - SDL_FreeSurface(hex_surf); - - // Slider bar - int bar_x = row.x + SCALE1(OPTION_PADDING) + label_w + SCALE1(OPTION_PADDING); - int bar_w = row.w - SCALE1(OPTION_PADDING) - label_w - SCALE1(OPTION_PADDING * 3) - hex_w; - int bar_h = SCALE1(6); - 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); - - // Capsule border: black outer ring, white inner ring (mirrors color preview style) - SDL_Rect border_outer = {bar_x - 2, bar_y - 2, bar_w + 4, bar_h + 4}; - drawSolidRoundedRect(surface, border_outer, bar_h / 2 + 2, black); - SDL_Rect border_inner = {bar_x - 1, bar_y - 1, bar_w + 2, bar_h + 2}; - drawSolidRoundedRect(surface, border_inner, bar_h / 2 + 1, white); - - // Gradient capsule - drawGradientCapsule(surface, {bar_x, bar_y, bar_w, bar_h}, this->r, this->g, this->b, channel); - - // Circle thumb indicator - const int circ_d = bar_h + SCALE1(4); // slightly larger than bar - const int circ_border = SCALE1(2); // black ring thickness (each side) - - int cx = bar_x + (int)((float)value / 255.0f * (bar_w - 1)); - int cy = bar_y + bar_h / 2; - // clamp so circle stays inside bar bounds - cx = std::max(bar_x + circ_d / 2, std::min(bar_x + bar_w - 1 - circ_d / 2, cx)); - - SDL_Rect outer_rect = {cx - circ_d / 2, cy - circ_d / 2, circ_d, circ_d}; - drawSolidRoundedRect(surface, outer_rect, circ_d / 2, black); - int inner_d = circ_d - circ_border * 2; - SDL_Rect inner_rect = {cx - inner_d / 2, cy - inner_d / 2, inner_d, inner_d}; - drawSolidRoundedRect(surface, inner_rect, inner_d / 2, white); - } -} - -void ColorPickerMenu::drawPreset(SDL_Surface *surface, const SDL_Rect &row, - const ColorPreset &preset, bool is_selected) -{ - if (is_selected) - GFX_blitPillLightCPP(ASSET_BUTTON, surface, row); - - // 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) -{ - 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 = currentColor(); - uint32_t col_mapped = SDL_MapRGB(surface->format, (col >> 16) & 0xFF, (col >> 8) & 0xFF, col & 0xFF); - - // R, G, B slider rows - const char *channel_labels[] = {"R", "G", "B"}; - int channel_values[] = {r, g, b}; - for (int i = 0; i < 3; 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 3-slider area - SDL_Rect preview = { - dst.x + SLIDER_AREA_W + SCALE1(PADDING), - dst.y + TOP_OFFSET + (SCALE1(BUTTON_SIZE * 3) - PREVIEW_SIZE) / 2, - PREVIEW_SIZE, - PREVIEW_SIZE - }; - drawRoundedRect(surface, preview, PREVIEW_RADIUS, white, black, 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 * 3) + SCALE1(i * BUTTON_SIZE), - dst.w, - SCALE1(BUTTON_SIZE) - }; - drawPreset(surface, row, presets[i], selected == 3 + i); - } -} static void drawOverlayLocal(SDL_Surface* screen) { // ReadLock r(overlayLock); // Assumes caller held lock or is safe diff --git a/workspace/all/settings/menu.hpp b/workspace/all/settings/menu.hpp index 6ca809432..e8a0e7310 100644 --- a/workspace/all/settings/menu.hpp +++ b/workspace/all/settings/menu.hpp @@ -375,31 +375,3 @@ const MenuListCallback ResetCurrentMenu = [](AbstractMenuItem &itm) -> InputReac return InputReactionHint::ResetAllItems; }; -struct ColorPreset { - uint32_t color; // 0xRRGGBB - std::string label; -}; - -class ColorPickerMenu : public MenuList -{ - int r, g, b; // 0–255 each - int selected; // 0=R, 1=G, 2=B, 3+N=presets - ValueSetCallback on_set; - std::vector presets; - - 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); - - void reset(uint32_t color, std::vector newPresets); - - InputReactionHint handleInput(int &dirty, int &quit) override; - void drawCustom(SDL_Surface *surface, const SDL_Rect &dst) override; -}; \ No newline at end of file diff --git a/workspace/all/settings/settings.cpp b/workspace/all/settings/settings.cpp index a5741a20f..901396fb7 100644 --- a/workspace/all/settings/settings.cpp +++ b/workspace/all/settings/settings.cpp @@ -15,6 +15,7 @@ extern "C" #include "wifimenu.hpp" #include "btmenu.hpp" #include "keyboardprompt.hpp" +#include "colorpickermenu.hpp" #define BUSYBOX_STOCK_VERSION "1.27.2" From d8456c4312f5ba7ce403aba94763b2cb78b2e1bf Mon Sep 17 00:00:00 2001 From: Tom Singleton Date: Thu, 26 Feb 2026 09:03:50 +0000 Subject: [PATCH 07/10] Color picker deduplication and magic number --- workspace/all/settings/colorpickermenu.cpp | 39 ++++++++++++---------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/workspace/all/settings/colorpickermenu.cpp b/workspace/all/settings/colorpickermenu.cpp index fd905d359..3aa0f146d 100644 --- a/workspace/all/settings/colorpickermenu.cpp +++ b/workspace/all/settings/colorpickermenu.cpp @@ -2,24 +2,29 @@ #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) : MenuList(MenuItemType::Custom, "", {}), on_set(on_set), - presets(std::move(presets)), r(0), g(0), b(0), selected(0) + presets(std::move(presets)), selected(0) { - r = (initialColor >> 16) & 0xFF; - g = (initialColor >> 8) & 0xFF; - b = initialColor & 0xFF; + decodeColor(initialColor, r, g, b); } void ColorPickerMenu::reset(uint32_t color, std::vector newPresets) { - r = (color >> 16) & 0xFF; - g = (color >> 8) & 0xFF; - b = color & 0xFF; + decodeColor(color, r, g, b); selected = 0; presets = std::move(newPresets); } @@ -37,7 +42,7 @@ void ColorPickerMenu::applyColor() InputReactionHint ColorPickerMenu::handleInput(int &dirty, int &quit) { - int total = 3 + (int)presets.size(); + int total = NUM_SLIDERS + (int)presets.size(); if (PAD_justRepeated(BTN_UP)) { @@ -63,7 +68,7 @@ InputReactionHint ColorPickerMenu::handleInput(int &dirty, int &quit) return NoOp; } - if (selected < 3) + if (selected < NUM_SLIDERS) { int *channel = (selected == 0) ? &r : (selected == 1) ? &g : &b; if (PAD_justRepeated(BTN_LEFT)) @@ -97,13 +102,11 @@ InputReactionHint ColorPickerMenu::handleInput(int &dirty, int &quit) } else { - int presetIdx = selected - 3; + int presetIdx = selected - NUM_SLIDERS; if (PAD_justPressed(BTN_A) && presetIdx < (int)presets.size()) { const auto &preset = presets[presetIdx]; - r = (preset.color >> 16) & 0xFF; - g = (preset.color >> 8) & 0xFF; - b = preset.color & 0xFF; + decodeColor(preset.color, r, g, b); applyColor(); selected = 0; dirty = 1; @@ -296,16 +299,16 @@ void ColorPickerMenu::drawCustom(SDL_Surface *surface, const SDL_Rect &dst) // R, G, B slider rows const char *channel_labels[] = {"R", "G", "B"}; int channel_values[] = {r, g, b}; - for (int i = 0; i < 3; i++) + 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 3-slider area + // 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 * 3) - PREVIEW_SIZE) / 2, + dst.y + TOP_OFFSET + (SCALE1(BUTTON_SIZE * NUM_SLIDERS) - PREVIEW_SIZE) / 2, PREVIEW_SIZE, PREVIEW_SIZE }; @@ -316,10 +319,10 @@ void ColorPickerMenu::drawCustom(SDL_Surface *surface, const SDL_Rect &dst) { SDL_Rect row = { dst.x, - dst.y + TOP_OFFSET + SCALE1(BUTTON_SIZE * 3) + SCALE1(i * BUTTON_SIZE), + dst.y + TOP_OFFSET + SCALE1(BUTTON_SIZE * NUM_SLIDERS) + SCALE1(i * BUTTON_SIZE), dst.w, SCALE1(BUTTON_SIZE) }; - drawPreset(surface, row, presets[i], selected == 3 + i); + drawPreset(surface, row, presets[i], selected == NUM_SLIDERS + i); } } From e8377618d8c04c5f1f5b1e4c77fb08dc8c2cf4d3 Mon Sep 17 00:00:00 2001 From: Tom Singleton Date: Thu, 26 Feb 2026 09:27:46 +0000 Subject: [PATCH 08/10] Settings color picker cleanup and refactor --- workspace/all/settings/colorpickermenu.cpp | 6 +- workspace/all/settings/settings.cpp | 66 +++++++++------------- 2 files changed, 30 insertions(+), 42 deletions(-) diff --git a/workspace/all/settings/colorpickermenu.cpp b/workspace/all/settings/colorpickermenu.cpp index 3aa0f146d..788504a49 100644 --- a/workspace/all/settings/colorpickermenu.cpp +++ b/workspace/all/settings/colorpickermenu.cpp @@ -70,7 +70,8 @@ InputReactionHint ColorPickerMenu::handleInput(int &dirty, int &quit) if (selected < NUM_SLIDERS) { - int *channel = (selected == 0) ? &r : (selected == 1) ? &g : &b; + int *channels[] = {&r, &g, &b}; + int *channel = channels[selected]; if (PAD_justRepeated(BTN_LEFT)) { *channel = std::max(0, *channel - 1); @@ -293,8 +294,7 @@ void ColorPickerMenu::drawCustom(SDL_Surface *surface, const SDL_Rect &dst) uint32_t white = SDL_MapRGB(surface->format, 255, 255, 255); uint32_t black = SDL_MapRGB(surface->format, 0, 0, 0); - uint32_t col = currentColor(); - uint32_t col_mapped = SDL_MapRGB(surface->format, (col >> 16) & 0xFF, (col >> 8) & 0xFF, col & 0xFF); + uint32_t col_mapped = SDL_MapRGB(surface->format, r, g, b); // R, G, B slider rows const char *channel_labels[] = {"R", "G", "B"}; diff --git a/workspace/all/settings/settings.cpp b/workspace/all/settings/settings.cpp index 901396fb7..db6944b1c 100644 --- a/workspace/all/settings/settings.cpp +++ b/workspace/all/settings/settings.cpp @@ -10,6 +10,7 @@ extern "C" #include #include +#include #include #include #include "wifimenu.hpp" @@ -298,14 +299,25 @@ 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 + auto makeColorSetter = [](int id) -> ValueSetCallback { + return [id](const std::any &v){ CFG_setColor(id, std::any_cast(v)); }; + }; + auto makeColorOpener = [](ColorPickerMenu *picker, int id) -> MenuListCallback { + return [picker, id](AbstractMenuItem &item) -> InputReactionHint { + picker->reset(CFG_getColor(id), buildColorPresets(id)); + return DeferToSubmenu(item); + }; + }; + // Pre-create one RGB picker per color setting (reused across opens) - auto *picker1 = new ColorPickerMenu(CFG_getColor(1), [](const std::any &v){ CFG_setColor(1, std::any_cast(v)); }, buildColorPresets(1)); - auto *picker2 = new ColorPickerMenu(CFG_getColor(2), [](const std::any &v){ CFG_setColor(2, std::any_cast(v)); }, buildColorPresets(2)); - auto *picker3 = new ColorPickerMenu(CFG_getColor(3), [](const std::any &v){ CFG_setColor(3, std::any_cast(v)); }, buildColorPresets(3)); - auto *picker4 = new ColorPickerMenu(CFG_getColor(4), [](const std::any &v){ CFG_setColor(4, std::any_cast(v)); }, buildColorPresets(4)); - auto *picker5 = new ColorPickerMenu(CFG_getColor(5), [](const std::any &v){ CFG_setColor(5, std::any_cast(v)); }, buildColorPresets(5)); - auto *picker6 = new ColorPickerMenu(CFG_getColor(6), [](const std::any &v){ CFG_setColor(6, std::any_cast(v)); }, buildColorPresets(6)); - auto *picker7 = new ColorPickerMenu(CFG_getColor(7), [](const std::any &v){ CFG_setColor(7, std::any_cast(v)); }, buildColorPresets(7)); + auto picker1 = std::make_unique(CFG_getColor(1), makeColorSetter(1), buildColorPresets(1)); + auto picker2 = std::make_unique(CFG_getColor(2), makeColorSetter(2), buildColorPresets(2)); + auto picker3 = std::make_unique(CFG_getColor(3), makeColorSetter(3), buildColorPresets(3)); + auto picker4 = std::make_unique(CFG_getColor(4), makeColorSetter(4), buildColorPresets(4)); + auto picker5 = std::make_unique(CFG_getColor(5), makeColorSetter(5), buildColorPresets(5)); + auto picker6 = std::make_unique(CFG_getColor(6), makeColorSetter(6), buildColorPresets(6)); + auto picker7 = std::make_unique(CFG_getColor(7), makeColorSetter(7), buildColorPresets(7)); auto appearanceMenu = new MenuList(MenuItemType::Fixed, "Appearance", {new MenuItem{ListItemType::Generic, "Font", "The font to render all UI text.", {0, 1}, font_names, @@ -316,58 +328,37 @@ int main(int argc, char *argv[]) []() -> std::any{ return CFG_getColor(1); }, [](const std::any &value){ CFG_setColor(1, std::any_cast(value)); }, []() { CFG_setColor(1, CFG_DEFAULT_COLOR1);}, - [picker1](AbstractMenuItem &item) -> InputReactionHint { - picker1->reset(CFG_getColor(1), buildColorPresets(1)); - return DeferToSubmenu(item); - }, picker1}, + makeColorOpener(picker1.get(), 1), picker1.get()}, 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(2); }, [](const std::any &value){ CFG_setColor(2, std::any_cast(value)); }, []() { CFG_setColor(2, CFG_DEFAULT_COLOR2);}, - [picker2](AbstractMenuItem &item) -> InputReactionHint { - picker2->reset(CFG_getColor(2), buildColorPresets(2)); - return DeferToSubmenu(item); - }, picker2}, + makeColorOpener(picker2.get(), 2), picker2.get()}, new MenuItem{ListItemType::Color, "Secondary Accent Color", "A secondary highlight color.", colors, color_strings, []() -> std::any{ return CFG_getColor(3); }, [](const std::any &value){ CFG_setColor(3, std::any_cast(value)); }, []() { CFG_setColor(3, CFG_DEFAULT_COLOR3);}, - [picker3](AbstractMenuItem &item) -> InputReactionHint { - picker3->reset(CFG_getColor(3), buildColorPresets(3)); - return DeferToSubmenu(item); - }, picker3}, + makeColorOpener(picker3.get(), 3), picker3.get()}, new MenuItem{ListItemType::Color, "Hint info Color", "Color for button hints and info", colors, color_strings, []() -> std::any{ return CFG_getColor(6); }, [](const std::any &value){ CFG_setColor(6, std::any_cast(value)); }, []() { CFG_setColor(6, CFG_DEFAULT_COLOR6);}, - [picker6](AbstractMenuItem &item) -> InputReactionHint { - picker6->reset(CFG_getColor(6), buildColorPresets(6)); - return DeferToSubmenu(item); - }, picker6}, + makeColorOpener(picker6.get(), 6), picker6.get()}, new MenuItem{ListItemType::Color, "List Text", "List text color", colors, color_strings, []() -> std::any{ return CFG_getColor(4); }, [](const std::any &value){ CFG_setColor(4, std::any_cast(value)); }, []() { CFG_setColor(4, CFG_DEFAULT_COLOR4);}, - [picker4](AbstractMenuItem &item) -> InputReactionHint { - picker4->reset(CFG_getColor(4), buildColorPresets(4)); - return DeferToSubmenu(item); - }, picker4}, + makeColorOpener(picker4.get(), 4), picker4.get()}, new MenuItem{ListItemType::Color, "List Text Selected", "List selected text color", colors, color_strings, []() -> std::any { return CFG_getColor(5); }, [](const std::any &value) { CFG_setColor(5, std::any_cast(value)); }, []() { CFG_setColor(5, CFG_DEFAULT_COLOR5);}, - [picker5](AbstractMenuItem &item) -> InputReactionHint { - picker5->reset(CFG_getColor(5), buildColorPresets(5)); - return DeferToSubmenu(item); - }, picker5}, + makeColorOpener(picker5.get(), 5), picker5.get()}, new MenuItem{ListItemType::Color, "Background Color", "Background color used when no background image is set.", colors, color_strings, []() -> std::any { return CFG_getColor(7); }, [](const std::any &value) { CFG_setColor(7, std::any_cast(value)); }, []() { CFG_setColor(7, CFG_DEFAULT_COLOR7);}, - [picker7](AbstractMenuItem &item) -> InputReactionHint { - picker7->reset(CFG_getColor(7), buildColorPresets(7)); - return DeferToSubmenu(item); - }, picker7}, + makeColorOpener(picker7.get(), 7), picker7.get()}, 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)); }, @@ -968,10 +959,7 @@ int main(int argc, char *argv[]) delete systemMenu; ctx.menu = NULL; - // Color pickers are not owned by their menu items (submenu deletion is - // commented out in AbstractMenuItem), so delete them explicitly. - delete picker1; delete picker2; delete picker3; - delete picker4; delete picker5; delete picker6; delete picker7; + // Color pickers are owned by unique_ptrs above; destroyed automatically here. QuitSettings(); PWR_quit(); From b03101d619e86499dcbc9112c02b377811610090 Mon Sep 17 00:00:00 2001 From: Tom Singleton Date: Thu, 26 Feb 2026 09:55:13 +0000 Subject: [PATCH 09/10] Further refactoring of color selection in settings --- workspace/all/settings/colorpickermenu.cpp | 2 +- workspace/all/settings/settings.cpp | 207 ++++++++++----------- 2 files changed, 96 insertions(+), 113 deletions(-) diff --git a/workspace/all/settings/colorpickermenu.cpp b/workspace/all/settings/colorpickermenu.cpp index 788504a49..2038f2ce1 100644 --- a/workspace/all/settings/colorpickermenu.cpp +++ b/workspace/all/settings/colorpickermenu.cpp @@ -16,7 +16,7 @@ static void decodeColor(uint32_t c, int &r, int &g, int &b) ColorPickerMenu::ColorPickerMenu(uint32_t initialColor, ValueSetCallback on_set, std::vector presets) - : MenuList(MenuItemType::Custom, "", {}), on_set(on_set), + : MenuList(MenuItemType::Custom, "", {}), on_set(std::move(on_set)), presets(std::move(presets)), selected(0) { decodeColor(initialColor, r, g, b); diff --git a/workspace/all/settings/settings.cpp b/workspace/all/settings/settings.cpp index db6944b1c..5bbfb2804 100644 --- a/workspace/all/settings/settings.cpp +++ b/workspace/all/settings/settings.cpp @@ -124,15 +124,15 @@ static const std::vector ra_sort_labels = { }; namespace { - struct ColorDef { int id; const char *name; }; + struct ColorDef { int id; const char *name; const char *desc; uint32_t defaultColor; }; static const ColorDef g_colorDefs[] = { - {1, "Main Color"}, - {2, "Primary Accent Color"}, - {3, "Secondary Accent Color"}, - {4, "List Text"}, - {5, "List Text Selected"}, - {6, "Hint info Color"}, - {7, "Background Color"}, + {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) @@ -309,112 +309,95 @@ int main(int argc, char *argv[]) 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) - auto picker1 = std::make_unique(CFG_getColor(1), makeColorSetter(1), buildColorPresets(1)); - auto picker2 = std::make_unique(CFG_getColor(2), makeColorSetter(2), buildColorPresets(2)); - auto picker3 = std::make_unique(CFG_getColor(3), makeColorSetter(3), buildColorPresets(3)); - auto picker4 = std::make_unique(CFG_getColor(4), makeColorSetter(4), buildColorPresets(4)); - auto picker5 = std::make_unique(CFG_getColor(5), makeColorSetter(5), buildColorPresets(5)); - auto picker6 = std::make_unique(CFG_getColor(6), makeColorSetter(6), buildColorPresets(6)); - auto picker7 = std::make_unique(CFG_getColor(7), makeColorSetter(7), buildColorPresets(7)); - - 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(1); }, - [](const std::any &value){ CFG_setColor(1, std::any_cast(value)); }, - []() { CFG_setColor(1, CFG_DEFAULT_COLOR1);}, - makeColorOpener(picker1.get(), 1), picker1.get()}, - 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(2); }, - [](const std::any &value){ CFG_setColor(2, std::any_cast(value)); }, - []() { CFG_setColor(2, CFG_DEFAULT_COLOR2);}, - makeColorOpener(picker2.get(), 2), picker2.get()}, - new MenuItem{ListItemType::Color, "Secondary Accent Color", "A secondary highlight color.", colors, color_strings, - []() -> std::any{ return CFG_getColor(3); }, - [](const std::any &value){ CFG_setColor(3, std::any_cast(value)); }, - []() { CFG_setColor(3, CFG_DEFAULT_COLOR3);}, - makeColorOpener(picker3.get(), 3), picker3.get()}, - new MenuItem{ListItemType::Color, "Hint info Color", "Color for button hints and info", colors, color_strings, - []() -> std::any{ return CFG_getColor(6); }, - [](const std::any &value){ CFG_setColor(6, std::any_cast(value)); }, - []() { CFG_setColor(6, CFG_DEFAULT_COLOR6);}, - makeColorOpener(picker6.get(), 6), picker6.get()}, - new MenuItem{ListItemType::Color, "List Text", "List text color", colors, color_strings, - []() -> std::any{ return CFG_getColor(4); }, - [](const std::any &value){ CFG_setColor(4, std::any_cast(value)); }, - []() { CFG_setColor(4, CFG_DEFAULT_COLOR4);}, - makeColorOpener(picker4.get(), 4), picker4.get()}, - new MenuItem{ListItemType::Color, "List Text Selected", "List selected text color", colors, color_strings, - []() -> std::any { return CFG_getColor(5); }, - [](const std::any &value) { CFG_setColor(5, std::any_cast(value)); }, - []() { CFG_setColor(5, CFG_DEFAULT_COLOR5);}, - makeColorOpener(picker5.get(), 5), picker5.get()}, - new MenuItem{ListItemType::Color, "Background Color", "Background color used when no background image is set.", colors, color_strings, - []() -> std::any { return CFG_getColor(7); }, - [](const std::any &value) { CFG_setColor(7, std::any_cast(value)); }, - []() { CFG_setColor(7, CFG_DEFAULT_COLOR7);}, - makeColorOpener(picker7.get(), 7), picker7.get()}, - 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 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}, - }); + 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))); + + // 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), 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 From 736bae9fbb781773c42ecfc37d1d18b7bdc9e70e Mon Sep 17 00:00:00 2001 From: Tom Singleton Date: Thu, 26 Feb 2026 15:33:55 +0000 Subject: [PATCH 10/10] Color picker page visual improvements --- workspace/all/settings/colorpickermenu.cpp | 129 ++++++++++++++++----- workspace/all/settings/colorpickermenu.hpp | 6 +- workspace/all/settings/settings.cpp | 10 +- 3 files changed, 107 insertions(+), 38 deletions(-) diff --git a/workspace/all/settings/colorpickermenu.cpp b/workspace/all/settings/colorpickermenu.cpp index 2038f2ce1..3c630003c 100644 --- a/workspace/all/settings/colorpickermenu.cpp +++ b/workspace/all/settings/colorpickermenu.cpp @@ -15,18 +15,21 @@ static void decodeColor(uint32_t c, int &r, int &g, int &b) // ColorPickerMenu ColorPickerMenu::ColorPickerMenu(uint32_t initialColor, ValueSetCallback on_set, - std::vector presets) + std::vector presets, std::string label) : MenuList(MenuItemType::Custom, "", {}), on_set(std::move(on_set)), - presets(std::move(presets)), selected(0) + 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) +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 @@ -64,9 +67,18 @@ InputReactionHint ColorPickerMenu::handleInput(int &dirty, int &quit) } 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) { @@ -193,31 +205,43 @@ void ColorPickerMenu::drawSlider(SDL_Surface *surface, const SDL_Rect &row, const char *label, int value, bool is_selected, int channel) { if (is_selected) - GFX_blitPillLightCPP(ASSET_BUTTON, surface, row); + drawSolidRoundedRect(surface, row, row.h / 2, THEME_COLOR1); SDL_Color text_color = is_selected ? uintToColour(THEME_COLOR5_255) : uintToColour(THEME_COLOR4_255); - // Channel label ("R", "G", "B") + // 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_w = label_surf->w; + int label_x = row.x + SCALE1(OPTION_PADDING) + (label_fixed_w - label_surf->w) / 2; SDL_BlitSurfaceCPP(label_surf, {}, surface, - {row.x + SCALE1(OPTION_PADDING), row.y + (row.h - label_surf->h) / 2}); + {label_x, row.y + (row.h - label_surf->h) / 2}); SDL_FreeSurface(label_surf); - // Hex value right-aligned ("FF", "00", etc.) + // 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_w = hex_surf->w; + int hex_slot_x = row.x + row.w - SCALE1(OPTION_PADDING) - hex_fixed_w; SDL_BlitSurfaceCPP(hex_surf, {}, surface, - {row.x + row.w - hex_w - SCALE1(OPTION_PADDING), + {hex_slot_x + (hex_fixed_w - hex_surf->w), row.y + (row.h - hex_surf->h) / 2}); SDL_FreeSurface(hex_surf); - // Slider bar - int bar_x = row.x + SCALE1(OPTION_PADDING) + label_w + SCALE1(OPTION_PADDING); - int bar_w = row.w - SCALE1(OPTION_PADDING) - label_w - SCALE1(OPTION_PADDING * 3) - hex_w; - int bar_h = SCALE1(6); + // 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) @@ -225,29 +249,42 @@ void ColorPickerMenu::drawSlider(SDL_Surface *surface, const SDL_Rect &row, uint32_t white = SDL_MapRGB(surface->format, 255, 255, 255); uint32_t black = SDL_MapRGB(surface->format, 0, 0, 0); - // Capsule border: black outer ring, white inner ring (mirrors color preview style) - SDL_Rect border_outer = {bar_x - 2, bar_y - 2, bar_w + 4, bar_h + 4}; - drawSolidRoundedRect(surface, border_outer, bar_h / 2 + 2, black); - SDL_Rect border_inner = {bar_x - 1, bar_y - 1, bar_w + 2, bar_h + 2}; - drawSolidRoundedRect(surface, border_inner, bar_h / 2 + 1, white); - - // Gradient capsule - drawGradientCapsule(surface, {bar_x, bar_y, bar_w, bar_h}, this->r, this->g, this->b, channel); + // 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(4); // slightly larger than bar - const int circ_border = SCALE1(2); // black ring thickness (each side) + 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; - // clamp so circle stays inside bar bounds cx = std::max(bar_x + circ_d / 2, std::min(bar_x + bar_w - 1 - circ_d / 2, cx)); - SDL_Rect outer_rect = {cx - circ_d / 2, cy - circ_d / 2, circ_d, circ_d}; - drawSolidRoundedRect(surface, outer_rect, circ_d / 2, black); + // 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 inner_rect = {cx - inner_d / 2, cy - inner_d / 2, inner_d, inner_d}; - drawSolidRoundedRect(surface, inner_rect, inner_d / 2, white); + 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); } } @@ -255,7 +292,7 @@ void ColorPickerMenu::drawPreset(SDL_Surface *surface, const SDL_Rect &row, const ColorPreset &preset, bool is_selected) { if (is_selected) - GFX_blitPillLightCPP(ASSET_BUTTON, surface, row); + drawSolidRoundedRect(surface, row, row.h / 2, THEME_COLOR1); // Small color square with white border int sq = SCALE1(FONT_TINY); @@ -287,6 +324,36 @@ void ColorPickerMenu::drawPreset(SDL_Surface *surface, const SDL_Rect &row, 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); @@ -312,7 +379,7 @@ void ColorPickerMenu::drawCustom(SDL_Surface *surface, const SDL_Rect &dst) PREVIEW_SIZE, PREVIEW_SIZE }; - drawRoundedRect(surface, preview, PREVIEW_RADIUS, white, black, col_mapped); + drawRoundedRect(surface, preview, PREVIEW_RADIUS, black, white, col_mapped); // Preset rows below the sliders for (int i = 0; i < (int)presets.size(); i++) diff --git a/workspace/all/settings/colorpickermenu.hpp b/workspace/all/settings/colorpickermenu.hpp index 991be481b..561c5acd0 100644 --- a/workspace/all/settings/colorpickermenu.hpp +++ b/workspace/all/settings/colorpickermenu.hpp @@ -13,6 +13,8 @@ class ColorPickerMenu : public MenuList int selected; ValueSetCallback on_set; std::vector presets; + std::string label_; + uint32_t originalColor_; uint32_t currentColor() const; void applyColor(); @@ -23,9 +25,9 @@ class ColorPickerMenu : public MenuList public: ColorPickerMenu(uint32_t initialColor, ValueSetCallback on_set, - std::vector presets); + std::vector presets, std::string label); - void reset(uint32_t color, std::vector newPresets); + 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/settings.cpp b/workspace/all/settings/settings.cpp index 5bbfb2804..9aaa21d5c 100644 --- a/workspace/all/settings/settings.cpp +++ b/workspace/all/settings/settings.cpp @@ -303,9 +303,9 @@ int main(int argc, char *argv[]) auto makeColorSetter = [](int id) -> ValueSetCallback { return [id](const std::any &v){ CFG_setColor(id, std::any_cast(v)); }; }; - auto makeColorOpener = [](ColorPickerMenu *picker, int id) -> MenuListCallback { - return [picker, id](AbstractMenuItem &item) -> InputReactionHint { - picker->reset(CFG_getColor(id), buildColorPresets(id)); + 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); }; }; @@ -321,7 +321,7 @@ int main(int argc, char *argv[]) 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))); + 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; @@ -335,7 +335,7 @@ int main(int argc, char *argv[]) makeColorGetter(def.id), makeColorSetter(def.id), makeColorResetter(def.id, def.defaultColor), - makeColorOpener(picker, def.id), picker}); + makeColorOpener(picker, def.id, def.name), picker}); } std::vector appearanceItems;