ui: light/dark theme toggle

This commit is contained in:
DeFiDude
2026-06-12 03:30:58 -06:00
parent 969267657e
commit 394a38e2ee
16 changed files with 249 additions and 82 deletions
+2
View File
@@ -78,6 +78,7 @@ bool UserConfig::parseJson(const String& json) {
if (rawBri > 100) rawBri = rawBri * 100 / 255; // Migrate from PWM to percentage
_settings.brightness = constrain(rawBri, 1, 100);
_settings.denseFontMode = doc["dense_font"] | false;
_settings.themeLight = doc["theme_light"] | false;
_settings.keyboardBrightness = constrain(doc["kb_brightness"] | 100, 0, 100);
_settings.keyboardAutoOn = doc["kb_auto_on"] | false;
_settings.keyboardAutoOff = doc["kb_auto_off"] | false;
@@ -150,6 +151,7 @@ String UserConfig::serializeToJson() const {
doc["screen_off"] = _settings.screenOffTimeout;
doc["brightness"] = _settings.brightness;
doc["dense_font"] = _settings.denseFontMode;
doc["theme_light"] = _settings.themeLight;
doc["kb_brightness"] = _settings.keyboardBrightness;
doc["kb_auto_on"] = _settings.keyboardAutoOn;
doc["kb_auto_off"] = _settings.keyboardAutoOff;
+1
View File
@@ -56,6 +56,7 @@ struct UserSettings {
uint16_t screenOffTimeout = 60; // seconds
uint8_t brightness = 100; // Percentage 1-100
bool denseFontMode = false; // T-Deck Plus: adaptive font toggle
bool themeLight = false; // false = dark (original palette)
// Keyboard
uint8_t keyboardBrightness = 100; // Percentage 0-100 (0 = off)
+8
View File
@@ -24,6 +24,7 @@
#include "input/InputManager.h"
#include "input/HotkeyManager.h"
#include "ui/UIManager.h"
#include "ui/Theme.h"
#include "ui/LvTabBar.h"
#include "ui/LvInput.h"
#include "ui/screens/LvBootScreen.h"
@@ -1106,6 +1107,8 @@ void setup() {
} else {
Serial.println("[BOOT] Early flash mount failed; using default radio config");
}
// Select palette before any LVGL styles are built
Theme::setScheme(userConfig.settings().themeLight ? Theme::Scheme::LIGHT : Theme::Scheme::DARK);
// Step 4: Radio + SD init BEFORE display
// Radio and SD must init while SPIClass exclusively owns SPI2_HOST.
@@ -1250,6 +1253,11 @@ void setup() {
lvBootScreen.setProgress(0.64f, "Loading config...");
userConfig.load(sdStore, flash);
// SD config may override the early flash-only load; re-sync palette
{
Theme::Scheme want = userConfig.settings().themeLight ? Theme::Scheme::LIGHT : Theme::Scheme::DARK;
if (want != Theme::scheme()) { Theme::setScheme(want); ui.applyTheme(); }
}
inputManager.setTrackballSpeed(userConfig.settings().trackballSpeed);
applyRadioSettingsToHardware(userConfig.settings(), "BOOT PRE-RNS");
+6
View File
@@ -212,6 +212,12 @@ void init(Keyboard* kb, Trackball* tb, TouchInput* touch) {
Serial.println("[LVGL] Input drivers registered (touch enabled)");
}
void applyTheme() {
if (!s_cursor) return;
lv_obj_set_style_bg_color(s_cursor, lv_color_hex(Theme::BG), 0);
lv_obj_set_style_outline_color(s_cursor, lv_color_hex(Theme::PRIMARY), 0);
}
void feedKey(const KeyEvent& evt) {
uint32_t key = 0;
+3
View File
@@ -18,4 +18,7 @@ void feedKey(const KeyEvent& evt);
// Get the LVGL input group (for focusing widgets)
lv_group_t* group();
// Re-apply palette colors to the touch cursor after a theme switch
void applyTheme();
} // namespace LvInput
+7
View File
@@ -203,6 +203,13 @@ void LvStatusBar::setUse24Hour(bool use24h) {
updateTime();
}
void LvStatusBar::applyTheme() {
if (_lblToast) lv_obj_set_style_text_color(_lblToast, lv_color_hex(Theme::BG), 0);
refreshIndicators();
refreshBattery();
refreshTimeColor();
}
void LvStatusBar::refreshIndicators() {
if (!_lblLinks) return;
+3
View File
@@ -26,6 +26,9 @@ public:
void setUse24Hour(bool use24h);
void updateTime(); // Call at 1 Hz to refresh clock
// Re-apply palette-dependent local colors after a theme switch
void applyTheme();
lv_obj_t* obj() { return _bar; }
private:
+3 -1
View File
@@ -29,8 +29,10 @@ public:
lv_obj_t* obj() { return _bar; }
private:
// Re-apply palette-dependent label colors after a theme switch
void refreshTabs();
private:
void refreshTab(int idx);
lv_obj_t* _bar = nullptr;
+86 -52
View File
@@ -37,34 +37,105 @@ static lv_style_t s_tabCell;
static lv_style_t s_tabCellActive;
static lv_style_t s_badge;
// Palette-dependent props only; re-run on scheme change.
static void applyColors() {
lv_style_set_bg_color(&s_screen, lv_color_hex(Theme::BG));
lv_style_set_text_color(&s_screen, lv_color_hex(Theme::TEXT_PRIMARY));
lv_style_set_text_color(&s_label, lv_color_hex(Theme::TEXT_PRIMARY));
lv_style_set_text_color(&s_labelMuted, lv_color_hex(Theme::TEXT_SECONDARY));
lv_style_set_text_color(&s_labelAccent, lv_color_hex(Theme::ACCENT));
lv_style_set_bg_color(&s_btn, lv_color_hex(Theme::BG_ELEVATED));
lv_style_set_border_color(&s_btn, lv_color_hex(Theme::BORDER));
lv_style_set_text_color(&s_btn, lv_color_hex(Theme::TEXT_PRIMARY));
lv_style_set_bg_color(&s_btnPressed, lv_color_hex(Theme::PRIMARY_SUBTLE));
lv_style_set_border_color(&s_btnPressed, lv_color_hex(Theme::PRIMARY));
lv_style_set_text_color(&s_btnPressed, lv_color_hex(Theme::ACCENT));
lv_style_set_border_color(&s_btnFocused, lv_color_hex(Theme::PRIMARY));
lv_style_set_outline_color(&s_btnFocused, lv_color_hex(Theme::ACCENT));
lv_style_set_bg_color(&s_bar, lv_color_hex(Theme::BORDER));
lv_style_set_bg_color(&s_barIndicator, lv_color_hex(Theme::PRIMARY));
lv_style_set_bg_color(&s_switch, lv_color_hex(Theme::BORDER));
lv_style_set_bg_color(&s_switchChecked, lv_color_hex(Theme::PRIMARY));
lv_style_set_bg_color(&s_textarea, lv_color_hex(Theme::BG_ELEVATED));
lv_style_set_border_color(&s_textarea, lv_color_hex(Theme::BORDER));
lv_style_set_text_color(&s_textarea, lv_color_hex(Theme::TEXT_PRIMARY));
lv_style_set_border_color(&s_textareaFocused, lv_color_hex(Theme::PRIMARY));
lv_style_set_bg_color(&s_list, lv_color_hex(Theme::BG));
lv_style_set_bg_color(&s_listBtn, lv_color_hex(Theme::BG));
lv_style_set_text_color(&s_listBtn, lv_color_hex(Theme::TEXT_PRIMARY));
lv_style_set_border_color(&s_listBtn, lv_color_hex(Theme::DIVIDER));
lv_style_set_bg_color(&s_listBtnFocused, lv_color_hex(Theme::BG_HOVER));
lv_style_set_text_color(&s_listBtnFocused, lv_color_hex(Theme::TEXT_PRIMARY));
lv_style_set_border_color(&s_listBtnFocused, lv_color_hex(Theme::PRIMARY));
lv_style_set_bg_color(&s_dropdown, lv_color_hex(Theme::BG_ELEVATED));
lv_style_set_border_color(&s_dropdown, lv_color_hex(Theme::BORDER));
lv_style_set_text_color(&s_dropdown, lv_color_hex(Theme::TEXT_PRIMARY));
lv_style_set_bg_color(&s_slider, lv_color_hex(Theme::BORDER));
lv_style_set_text_color(&s_sectionHeader, lv_color_hex(Theme::TEXT_SECONDARY));
lv_style_set_border_color(&s_sectionHeader, lv_color_hex(Theme::DIVIDER));
lv_style_set_bg_color(&s_modal, lv_color_hex(Theme::BG_SURFACE));
lv_style_set_border_color(&s_modal, lv_color_hex(Theme::BORDER));
lv_style_set_bg_color(&s_scrollbar, lv_color_hex(Theme::TEXT_MUTED));
lv_style_set_bg_color(&s_roller, lv_color_hex(Theme::BG));
lv_style_set_text_color(&s_roller, lv_color_hex(Theme::TEXT_SECONDARY));
lv_style_set_bg_color(&s_statusBar, lv_color_hex(Theme::STATUS_BG));
lv_style_set_border_color(&s_statusBar, lv_color_hex(Theme::DIVIDER));
lv_style_set_bg_color(&s_statusToast, lv_color_hex(Theme::TOAST_BG));
lv_style_set_text_color(&s_statusToast, lv_color_hex(Theme::BG));
lv_style_set_bg_color(&s_tabBar, lv_color_hex(Theme::TAB_BG));
lv_style_set_border_color(&s_tabBar, lv_color_hex(Theme::DIVIDER));
lv_style_set_bg_color(&s_tabCell, lv_color_hex(Theme::TAB_BG));
lv_style_set_text_color(&s_tabCell, lv_color_hex(Theme::TAB_INACTIVE));
lv_style_set_bg_color(&s_tabCellActive, lv_color_hex(Theme::TAB_ACTIVE_BG));
lv_style_set_border_color(&s_tabCellActive, lv_color_hex(Theme::PRIMARY));
lv_style_set_text_color(&s_tabCellActive, lv_color_hex(Theme::TAB_ACTIVE));
lv_style_set_bg_color(&s_badge, lv_color_hex(Theme::BADGE_BG));
lv_style_set_text_color(&s_badge, lv_color_hex(Theme::BG));
}
void init(lv_disp_t* disp) {
// Screen background (LV_USE_THEME_DEFAULT is disabled in lv_conf.h).
lv_style_init(&s_screen);
lv_style_set_bg_color(&s_screen, lv_color_hex(Theme::BG));
lv_style_set_bg_opa(&s_screen, LV_OPA_COVER);
lv_style_set_text_color(&s_screen, lv_color_hex(Theme::TEXT_PRIMARY));
lv_style_set_text_font(&s_screen, &lv_font_ratdeck_14);
// Labels
lv_style_init(&s_label);
lv_style_set_text_color(&s_label, lv_color_hex(Theme::TEXT_PRIMARY));
lv_style_set_text_font(&s_label, &lv_font_ratdeck_14);
lv_style_init(&s_labelMuted);
lv_style_set_text_color(&s_labelMuted, lv_color_hex(Theme::TEXT_SECONDARY));
lv_style_set_text_font(&s_labelMuted, &lv_font_ratdeck_12);
lv_style_init(&s_labelAccent);
lv_style_set_text_color(&s_labelAccent, lv_color_hex(Theme::ACCENT));
lv_style_set_text_font(&s_labelAccent, &lv_font_ratdeck_14);
// Buttons
lv_style_init(&s_btn);
lv_style_set_bg_color(&s_btn, lv_color_hex(Theme::BG_ELEVATED));
lv_style_set_bg_opa(&s_btn, LV_OPA_COVER);
lv_style_set_border_color(&s_btn, lv_color_hex(Theme::BORDER));
lv_style_set_border_width(&s_btn, 1);
lv_style_set_text_color(&s_btn, lv_color_hex(Theme::TEXT_PRIMARY));
lv_style_set_text_font(&s_btn, &lv_font_ratdeck_12);
lv_style_set_radius(&s_btn, 3);
lv_style_set_pad_top(&s_btn, 5);
@@ -73,56 +144,42 @@ void init(lv_disp_t* disp) {
lv_style_set_pad_right(&s_btn, 8);
lv_style_init(&s_btnPressed);
lv_style_set_bg_color(&s_btnPressed, lv_color_hex(Theme::PRIMARY_SUBTLE));
lv_style_set_border_color(&s_btnPressed, lv_color_hex(Theme::PRIMARY));
lv_style_set_border_width(&s_btnPressed, 1);
lv_style_set_text_color(&s_btnPressed, lv_color_hex(Theme::ACCENT));
lv_style_init(&s_btnFocused);
lv_style_set_border_color(&s_btnFocused, lv_color_hex(Theme::PRIMARY));
lv_style_set_border_width(&s_btnFocused, 2);
lv_style_set_outline_color(&s_btnFocused, lv_color_hex(Theme::ACCENT));
lv_style_set_outline_width(&s_btnFocused, 1);
lv_style_set_outline_pad(&s_btnFocused, 0);
// Bar
lv_style_init(&s_bar);
lv_style_set_bg_color(&s_bar, lv_color_hex(Theme::BORDER));
lv_style_set_bg_opa(&s_bar, LV_OPA_COVER);
lv_style_set_radius(&s_bar, 2);
lv_style_init(&s_barIndicator);
lv_style_set_bg_color(&s_barIndicator, lv_color_hex(Theme::PRIMARY));
lv_style_set_bg_opa(&s_barIndicator, LV_OPA_COVER);
lv_style_set_radius(&s_barIndicator, 2);
// Switch (LV_USE_SWITCH currently 0; kept for future lv_menu rewrite)
lv_style_init(&s_switch);
lv_style_set_bg_color(&s_switch, lv_color_hex(Theme::BORDER));
lv_style_set_bg_opa(&s_switch, LV_OPA_COVER);
lv_style_set_radius(&s_switch, LV_RADIUS_CIRCLE);
lv_style_init(&s_switchChecked);
lv_style_set_bg_color(&s_switchChecked, lv_color_hex(Theme::PRIMARY));
// Textarea
lv_style_init(&s_textarea);
lv_style_set_bg_color(&s_textarea, lv_color_hex(Theme::BG_ELEVATED));
lv_style_set_bg_opa(&s_textarea, LV_OPA_COVER);
lv_style_set_border_color(&s_textarea, lv_color_hex(Theme::BORDER));
lv_style_set_border_width(&s_textarea, 1);
lv_style_set_text_color(&s_textarea, lv_color_hex(Theme::TEXT_PRIMARY));
lv_style_set_text_font(&s_textarea, &lv_font_ratdeck_14);
lv_style_set_radius(&s_textarea, 3);
lv_style_set_pad_all(&s_textarea, 6);
lv_style_init(&s_textareaFocused);
lv_style_set_border_color(&s_textareaFocused, lv_color_hex(Theme::PRIMARY));
lv_style_set_border_width(&s_textareaFocused, 1);
// List container
lv_style_init(&s_list);
lv_style_set_bg_color(&s_list, lv_color_hex(Theme::BG));
lv_style_set_bg_opa(&s_list, LV_OPA_COVER);
lv_style_set_pad_all(&s_list, 0);
lv_style_set_pad_row(&s_list, 0);
@@ -131,11 +188,8 @@ void init(lv_disp_t* disp) {
// List items
lv_style_init(&s_listBtn);
lv_style_set_bg_color(&s_listBtn, lv_color_hex(Theme::BG));
lv_style_set_bg_opa(&s_listBtn, LV_OPA_COVER);
lv_style_set_text_color(&s_listBtn, lv_color_hex(Theme::TEXT_PRIMARY));
lv_style_set_text_font(&s_listBtn, &lv_font_ratdeck_12);
lv_style_set_border_color(&s_listBtn, lv_color_hex(Theme::DIVIDER));
lv_style_set_border_width(&s_listBtn, 1);
lv_style_set_border_side(&s_listBtn, LV_BORDER_SIDE_BOTTOM);
lv_style_set_pad_top(&s_listBtn, 5);
@@ -146,37 +200,28 @@ void init(lv_disp_t* disp) {
// List item focused - left-bar indicator + hover background
lv_style_init(&s_listBtnFocused);
lv_style_set_bg_color(&s_listBtnFocused, lv_color_hex(Theme::BG_HOVER));
lv_style_set_bg_opa(&s_listBtnFocused, LV_OPA_COVER);
lv_style_set_text_color(&s_listBtnFocused, lv_color_hex(Theme::TEXT_PRIMARY));
lv_style_set_border_color(&s_listBtnFocused, lv_color_hex(Theme::PRIMARY));
lv_style_set_border_width(&s_listBtnFocused, 3);
lv_style_set_border_side(&s_listBtnFocused, LV_BORDER_SIDE_LEFT);
// Dropdown
lv_style_init(&s_dropdown);
lv_style_set_bg_color(&s_dropdown, lv_color_hex(Theme::BG_ELEVATED));
lv_style_set_bg_opa(&s_dropdown, LV_OPA_COVER);
lv_style_set_border_color(&s_dropdown, lv_color_hex(Theme::BORDER));
lv_style_set_border_width(&s_dropdown, 1);
lv_style_set_text_color(&s_dropdown, lv_color_hex(Theme::TEXT_PRIMARY));
lv_style_set_text_font(&s_dropdown, &lv_font_ratdeck_12);
lv_style_set_radius(&s_dropdown, 3);
lv_style_set_pad_all(&s_dropdown, 4);
// Slider
lv_style_init(&s_slider);
lv_style_set_bg_color(&s_slider, lv_color_hex(Theme::BORDER));
lv_style_set_bg_opa(&s_slider, LV_OPA_COVER);
lv_style_set_radius(&s_slider, 3);
// Section header (for list section dividers like "Contacts (3)")
lv_style_init(&s_sectionHeader);
lv_style_set_bg_opa(&s_sectionHeader, LV_OPA_TRANSP);
lv_style_set_text_color(&s_sectionHeader, lv_color_hex(Theme::TEXT_SECONDARY));
lv_style_set_text_font(&s_sectionHeader, &lv_font_ratdeck_10);
lv_style_set_text_letter_space(&s_sectionHeader, 0);
lv_style_set_border_color(&s_sectionHeader, lv_color_hex(Theme::DIVIDER));
lv_style_set_border_width(&s_sectionHeader, 1);
lv_style_set_border_side(&s_sectionHeader, LV_BORDER_SIDE_BOTTOM);
lv_style_set_pad_top(&s_sectionHeader, 8);
@@ -185,9 +230,7 @@ void init(lv_disp_t* disp) {
// Modal overlay
lv_style_init(&s_modal);
lv_style_set_bg_color(&s_modal, lv_color_hex(Theme::BG_SURFACE));
lv_style_set_bg_opa(&s_modal, LV_OPA_COVER);
lv_style_set_border_color(&s_modal, lv_color_hex(Theme::BORDER));
lv_style_set_border_width(&s_modal, 1);
lv_style_set_radius(&s_modal, 4);
lv_style_set_pad_all(&s_modal, 10);
@@ -198,7 +241,6 @@ void init(lv_disp_t* disp) {
// Scrollbar
lv_style_init(&s_scrollbar);
lv_style_set_bg_color(&s_scrollbar, lv_color_hex(Theme::TEXT_MUTED));
lv_style_set_bg_opa(&s_scrollbar, LV_OPA_40);
lv_style_set_radius(&s_scrollbar, LV_RADIUS_CIRCLE);
lv_style_set_width(&s_scrollbar, 3);
@@ -206,34 +248,26 @@ void init(lv_disp_t* disp) {
// Roller - no border so selected row highlight extends edge-to-edge
lv_style_init(&s_roller);
lv_style_set_bg_color(&s_roller, lv_color_hex(Theme::BG));
lv_style_set_bg_opa(&s_roller, LV_OPA_COVER);
lv_style_set_text_color(&s_roller, lv_color_hex(Theme::TEXT_SECONDARY));
lv_style_set_text_font(&s_roller, &lv_font_ratdeck_14);
lv_style_set_border_width(&s_roller, 0);
// Persistent shell bars.
lv_style_init(&s_statusBar);
lv_style_set_bg_color(&s_statusBar, lv_color_hex(Theme::STATUS_BG));
lv_style_set_bg_opa(&s_statusBar, LV_OPA_COVER);
lv_style_set_border_color(&s_statusBar, lv_color_hex(Theme::DIVIDER));
lv_style_set_border_width(&s_statusBar, 1);
lv_style_set_border_side(&s_statusBar, LV_BORDER_SIDE_BOTTOM);
lv_style_set_pad_all(&s_statusBar, 0);
lv_style_set_radius(&s_statusBar, 0);
lv_style_init(&s_statusToast);
lv_style_set_bg_color(&s_statusToast, lv_color_hex(Theme::TOAST_BG));
lv_style_set_bg_opa(&s_statusToast, LV_OPA_COVER);
lv_style_set_text_color(&s_statusToast, lv_color_hex(Theme::BG));
lv_style_set_text_font(&s_statusToast, &lv_font_ratdeck_12);
lv_style_set_pad_all(&s_statusToast, 0);
lv_style_set_radius(&s_statusToast, 0);
lv_style_init(&s_tabBar);
lv_style_set_bg_color(&s_tabBar, lv_color_hex(Theme::TAB_BG));
lv_style_set_bg_opa(&s_tabBar, LV_OPA_COVER);
lv_style_set_border_color(&s_tabBar, lv_color_hex(Theme::DIVIDER));
lv_style_set_border_width(&s_tabBar, 1);
lv_style_set_border_side(&s_tabBar, LV_BORDER_SIDE_TOP);
lv_style_set_pad_all(&s_tabBar, 0);
@@ -241,36 +275,36 @@ void init(lv_disp_t* disp) {
lv_style_set_radius(&s_tabBar, 0);
lv_style_init(&s_tabCell);
lv_style_set_bg_color(&s_tabCell, lv_color_hex(Theme::TAB_BG));
lv_style_set_bg_opa(&s_tabCell, LV_OPA_COVER);
lv_style_set_border_width(&s_tabCell, 0);
lv_style_set_text_color(&s_tabCell, lv_color_hex(Theme::TAB_INACTIVE));
lv_style_set_text_font(&s_tabCell, &lv_font_ratdeck_10);
lv_style_set_pad_all(&s_tabCell, 0);
lv_style_set_radius(&s_tabCell, 0);
lv_style_init(&s_tabCellActive);
lv_style_set_bg_color(&s_tabCellActive, lv_color_hex(Theme::TAB_ACTIVE_BG));
lv_style_set_bg_opa(&s_tabCellActive, LV_OPA_COVER);
lv_style_set_border_color(&s_tabCellActive, lv_color_hex(Theme::PRIMARY));
lv_style_set_border_width(&s_tabCellActive, 2);
lv_style_set_border_side(&s_tabCellActive, LV_BORDER_SIDE_TOP);
lv_style_set_text_color(&s_tabCellActive, lv_color_hex(Theme::TAB_ACTIVE));
lv_style_init(&s_badge);
lv_style_set_bg_color(&s_badge, lv_color_hex(Theme::BADGE_BG));
lv_style_set_bg_opa(&s_badge, LV_OPA_COVER);
lv_style_set_text_color(&s_badge, lv_color_hex(Theme::BG));
lv_style_set_text_font(&s_badge, &lv_font_ratdeck_10);
lv_style_set_radius(&s_badge, 3);
lv_style_set_pad_all(&s_badge, 0);
applyColors();
// Apply screen style to default theme
lv_obj_add_style(lv_scr_act(), &s_screen, 0);
Serial.println("[LVGL] Ratdeck field-console theme initialized");
}
void refresh() {
applyColors();
lv_obj_report_style_change(NULL);
}
// Existing accessors (16)
lv_style_t* styleScreen() { return &s_screen; }
lv_style_t* styleLabel() { return &s_label; }
+3
View File
@@ -7,6 +7,9 @@ namespace LvTheme {
void init(lv_disp_t* disp);
// Re-apply palette colors to all shared styles after a Theme scheme change.
void refresh();
// Style accessors - existing (16)
lv_style_t* styleScreen();
lv_style_t* styleLabel();
+55
View File
@@ -0,0 +1,55 @@
#include "Theme.h"
// Dark column is the original Ratdeck palette, verbatim. Light column is
// derived: same hue family, brand greens/cyan darkened for light-bg contrast.
#define THEME_COLORS(X) \
X(BG, 0x05080A, 0xF2F6F4) \
X(BG_ELEVATED, 0x0C1418, 0xFFFFFF) \
X(BG_SURFACE, 0x121D23, 0xE4ECE9) \
X(BG_HOVER, 0x183033, 0xD5E5DE) \
X(TEXT_PRIMARY, 0xE4F3F0, 0x0E1F1B) \
X(TEXT_SECONDARY, 0x91A8A5, 0x47615C) \
X(TEXT_MUTED, 0x526866, 0x8CA29D) \
X(PRIMARY, 0x00E06D, 0x008F4C) \
X(PRIMARY_MUTED, 0x00A853, 0x00713C) \
X(PRIMARY_SUBTLE, 0x06251A, 0xD7F0E1) \
X(ACCENT, 0x4FD7FF, 0x0077A8) \
X(SUCCESS, 0x31E981, 0x168A4D) \
X(WARNING_CLR, 0xF0C04F, 0x9A7400) \
X(ERROR_CLR, 0xFF5C6C, 0xD02E44) \
X(ERROR_SUBTLE, 0x2A0E0E, 0xF6DBDD) \
X(BORDER, 0x21383B, 0xC2D3CE) \
X(BORDER_ACTIVE, 0x00E06D, 0x008F4C) \
X(DIVIDER, 0x102329, 0xDCE6E2) \
X(MSG_OUT_BG, 0x082719, 0xD7F0E1) \
X(MSG_IN_BG, 0x0B151C, 0xE7EDF1) \
X(STATUS_BG, 0x071014, 0xE4ECE9) \
X(STATUS_FLASH, 0x12351F, 0xBFE8CE) \
X(TAB_BG, 0x081115, 0xE4ECE9) \
X(TAB_ACTIVE_BG, 0x0B241B, 0xD7F0E1) \
X(TAB_ACTIVE, 0x00E06D, 0x00713C) \
X(TAB_INACTIVE, 0x91A8A5, 0x47615C) \
X(BADGE_BG, 0xFF5C6C, 0xD02E44) \
X(TOAST_BG, 0x00E06D, 0x008F4C)
namespace Theme {
namespace {
Scheme g_scheme = Scheme::DARK;
}
#define THEME_DEFINE(name, dark, light) uint32_t name = dark;
THEME_COLORS(THEME_DEFINE)
#undef THEME_DEFINE
void setScheme(Scheme s) {
g_scheme = s;
const bool light = (s == Scheme::LIGHT);
#define THEME_APPLY(name, d, l) name = light ? (uint32_t)(l) : (uint32_t)(d);
THEME_COLORS(THEME_APPLY)
#undef THEME_APPLY
}
Scheme scheme() { return g_scheme; }
} // namespace Theme
+38 -28
View File
@@ -4,51 +4,61 @@
// =============================================================================
// Ratdeck design constants
// Dark field-console palette tuned for the 320x240 LVGL surface.
// Field-console palette tuned for the 320x240 LVGL surface.
// Colors are runtime-switchable (Dark = original palette, Light = derived);
// the value tables live in Theme.cpp. Identifiers keep their original
// spelling so call sites read the active palette transparently.
// =============================================================================
namespace Theme {
enum class Scheme : uint8_t { DARK = 0, LIGHT = 1 };
// Switch the active palette (boot default: DARK, the historical palette).
void setScheme(Scheme s);
Scheme scheme();
// --- Backgrounds ---
constexpr uint32_t BG = 0x05080A; // Screen base
constexpr uint32_t BG_ELEVATED = 0x0C1418; // Buttons, inputs, list items
constexpr uint32_t BG_SURFACE = 0x121D23; // Modals, dropdowns, shell bars
constexpr uint32_t BG_HOVER = 0x183033; // Focus/hover background
extern uint32_t BG; // Screen base
extern uint32_t BG_ELEVATED; // Buttons, inputs, list items
extern uint32_t BG_SURFACE; // Modals, dropdowns, shell bars
extern uint32_t BG_HOVER; // Focus/hover background
// --- Text hierarchy ---
constexpr uint32_t TEXT_PRIMARY = 0xE4F3F0; // Primary copy
constexpr uint32_t TEXT_SECONDARY = 0x91A8A5; // Metadata and labels
constexpr uint32_t TEXT_MUTED = 0x526866; // Disabled, placeholders
extern uint32_t TEXT_PRIMARY; // Primary copy
extern uint32_t TEXT_SECONDARY; // Metadata and labels
extern uint32_t TEXT_MUTED; // Disabled, placeholders
// --- Brand / interactive ---
constexpr uint32_t PRIMARY = 0x00E06D; // Signal green
constexpr uint32_t PRIMARY_MUTED = 0x00A853; // Pressed/active state
constexpr uint32_t PRIMARY_SUBTLE = 0x06251A; // Selection backgrounds
constexpr uint32_t ACCENT = 0x4FD7FF; // Console cyan accent
extern uint32_t PRIMARY; // Signal green
extern uint32_t PRIMARY_MUTED; // Pressed/active state
extern uint32_t PRIMARY_SUBTLE; // Selection backgrounds
extern uint32_t ACCENT; // Console cyan accent
// --- Status ---
constexpr uint32_t SUCCESS = 0x31E981; // Online, delivered, connected
constexpr uint32_t WARNING_CLR = 0xF0C04F; // Queued, pending, caution
constexpr uint32_t ERROR_CLR = 0xFF5C6C; // Failed, error, critical
extern uint32_t SUCCESS; // Online, delivered, connected
extern uint32_t WARNING_CLR; // Queued, pending, caution
extern uint32_t ERROR_CLR; // Failed, error, critical
extern uint32_t ERROR_SUBTLE; // Armed destructive backgrounds
// --- Structural ---
constexpr uint32_t BORDER = 0x21383B; // Subtle structural borders
constexpr uint32_t BORDER_ACTIVE = 0x00E06D; // Focus borders
constexpr uint32_t DIVIDER = 0x102329; // Hairline separators
extern uint32_t BORDER; // Subtle structural borders
extern uint32_t BORDER_ACTIVE; // Focus borders
extern uint32_t DIVIDER; // Hairline separators
// --- Messages ---
constexpr uint32_t MSG_OUT_BG = 0x082719; // Outgoing bubble
constexpr uint32_t MSG_IN_BG = 0x0B151C; // Incoming bubble
extern uint32_t MSG_OUT_BG; // Outgoing bubble
extern uint32_t MSG_IN_BG; // Incoming bubble
// --- Shell ---
constexpr uint32_t STATUS_BG = 0x071014;
constexpr uint32_t STATUS_FLASH = 0x12351F;
constexpr uint32_t TAB_BG = 0x081115;
constexpr uint32_t TAB_ACTIVE_BG = 0x0B241B;
constexpr uint32_t TAB_ACTIVE = 0x00E06D;
constexpr uint32_t TAB_INACTIVE = 0x91A8A5;
constexpr uint32_t BADGE_BG = 0xFF5C6C;
constexpr uint32_t TOAST_BG = 0x00E06D;
extern uint32_t STATUS_BG;
extern uint32_t STATUS_FLASH;
extern uint32_t TAB_BG;
extern uint32_t TAB_ACTIVE_BG;
extern uint32_t TAB_ACTIVE;
extern uint32_t TAB_INACTIVE;
extern uint32_t BADGE_BG;
extern uint32_t TOAST_BG;
// --- Layout Metrics ---
+10
View File
@@ -1,6 +1,7 @@
#include "UIManager.h"
#include "Theme.h"
#include "LvTheme.h"
#include "LvInput.h"
// --- LvScreen base ---
@@ -90,6 +91,15 @@ void UIManager::forceRedraw() {
lv_obj_invalidate(lv_scr_act());
}
void UIManager::applyTheme() {
LvTheme::refresh();
if (_lvContent) lv_obj_set_style_bg_color(_lvContent, lv_color_hex(Theme::BG), 0);
_lvStatusBar.applyTheme();
_lvTabBar.refreshTabs();
LvInput::applyTheme();
forceRedraw();
}
bool UIManager::handleKey(const KeyEvent& event) {
if (_currentLvScreen) {
return _currentLvScreen->handleKey(event);
+3
View File
@@ -42,6 +42,9 @@ public:
// Force full redraw
void forceRedraw();
// Re-apply palette to shared styles and persistent shell after a theme switch
void applyTheme();
// Handle key event — routes to current screen
bool handleKey(const KeyEvent& event);
bool handleLongPress();
+1 -1
View File
@@ -38,7 +38,7 @@ void styleChoice(lv_obj_t* box, bool selected, bool destructive, bool armed) {
if (!box) return;
uint32_t border = selected ? (destructive && armed ? Theme::ERROR_CLR : Theme::PRIMARY) : Theme::BORDER;
uint32_t bg = selected ? Theme::PRIMARY_SUBTLE : Theme::BG_ELEVATED;
if (destructive && armed) bg = 0x2A0E0E;
if (destructive && armed) bg = Theme::ERROR_SUBTLE;
lv_obj_set_style_bg_color(box, lv_color_hex(bg), 0);
lv_obj_set_style_border_color(box, lv_color_hex(border), 0);
lv_obj_set_style_border_width(box, selected ? 2 : 1, 0);
+20
View File
@@ -480,6 +480,17 @@ void LvSettingsScreen::buildItems() {
[&s]() { return s.brightness; }, [&s](int v) { s.brightness = v; },
[](int v) { return String(v) + "%"; }, 5, 100, 5});
idx++;
{
SettingItem themeItem;
themeItem.label = "Theme";
themeItem.type = SettingType::ENUM_CHOICE;
themeItem.getter = [&s]() { return s.themeLight ? 1 : 0; };
themeItem.setter = [&s](int v) { s.themeLight = (v != 0); };
themeItem.minVal = 0; themeItem.maxVal = 1; themeItem.step = 1;
themeItem.enumLabels = {"Dark", "Light"};
_items.push_back(themeItem);
idx++;
}
_items.push_back({"Dim After", SettingType::INTEGER,
[&s]() { return s.screenDimTimeout; }, [&s](int v) { s.screenDimTimeout = v; },
[](int v) { return String(v) + "s"; }, 5, 300, 5});
@@ -2024,6 +2035,15 @@ String LvSettingsScreen::freqFormatWithCursor() const {
void LvSettingsScreen::applyAndSave() {
if (!_cfg) return;
auto& s = _cfg->settings();
// Theme switch applies live: palette globals -> shared styles -> shell.
// Our own rows re-read Theme:: on the rebuild that follows every commit.
Theme::Scheme want = s.themeLight ? Theme::Scheme::LIGHT : Theme::Scheme::DARK;
if (want != Theme::scheme()) {
Theme::setScheme(want);
if (_ui) _ui->applyTheme();
if (_screen) lv_obj_set_style_bg_color(_screen, lv_color_hex(Theme::BG), 0);
if (_scrollContainer) lv_obj_set_style_bg_color(_scrollContainer, lv_color_hex(Theme::BG), 0);
}
if (_power) {
_power->setBrightness(s.brightness);
_power->setDimTimeout(s.screenDimTimeout);