diff --git a/src/config/UserConfig.cpp b/src/config/UserConfig.cpp index 9ac3aac..685e0e0 100644 --- a/src/config/UserConfig.cpp +++ b/src/config/UserConfig.cpp @@ -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; diff --git a/src/config/UserConfig.h b/src/config/UserConfig.h index 00ad06e..38c67cd 100644 --- a/src/config/UserConfig.h +++ b/src/config/UserConfig.h @@ -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) diff --git a/src/main.cpp b/src/main.cpp index 1bfdf97..454c12a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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"); diff --git a/src/ui/LvInput.cpp b/src/ui/LvInput.cpp index 7748e07..a50eef2 100644 --- a/src/ui/LvInput.cpp +++ b/src/ui/LvInput.cpp @@ -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; diff --git a/src/ui/LvInput.h b/src/ui/LvInput.h index 4ef0d3a..031525d 100644 --- a/src/ui/LvInput.h +++ b/src/ui/LvInput.h @@ -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 diff --git a/src/ui/LvStatusBar.cpp b/src/ui/LvStatusBar.cpp index 228f0f8..066ca28 100644 --- a/src/ui/LvStatusBar.cpp +++ b/src/ui/LvStatusBar.cpp @@ -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; diff --git a/src/ui/LvStatusBar.h b/src/ui/LvStatusBar.h index 91cd6d7..0c162d0 100644 --- a/src/ui/LvStatusBar.h +++ b/src/ui/LvStatusBar.h @@ -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: diff --git a/src/ui/LvTabBar.h b/src/ui/LvTabBar.h index bb8a014..624aa9f 100644 --- a/src/ui/LvTabBar.h +++ b/src/ui/LvTabBar.h @@ -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; diff --git a/src/ui/LvTheme.cpp b/src/ui/LvTheme.cpp index 0f4c893..5df98a9 100644 --- a/src/ui/LvTheme.cpp +++ b/src/ui/LvTheme.cpp @@ -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; } diff --git a/src/ui/LvTheme.h b/src/ui/LvTheme.h index 2d38bef..8b0757d 100644 --- a/src/ui/LvTheme.h +++ b/src/ui/LvTheme.h @@ -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(); diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp new file mode 100644 index 0000000..5b4bd44 --- /dev/null +++ b/src/ui/Theme.cpp @@ -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 diff --git a/src/ui/Theme.h b/src/ui/Theme.h index d0a73c4..276bb55 100644 --- a/src/ui/Theme.h +++ b/src/ui/Theme.h @@ -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 --- diff --git a/src/ui/UIManager.cpp b/src/ui/UIManager.cpp index 2500f32..163aba3 100644 --- a/src/ui/UIManager.cpp +++ b/src/ui/UIManager.cpp @@ -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); diff --git a/src/ui/UIManager.h b/src/ui/UIManager.h index 73a8772..afb834f 100644 --- a/src/ui/UIManager.h +++ b/src/ui/UIManager.h @@ -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(); diff --git a/src/ui/screens/LvDataCleanScreen.cpp b/src/ui/screens/LvDataCleanScreen.cpp index df3b333..4bae772 100644 --- a/src/ui/screens/LvDataCleanScreen.cpp +++ b/src/ui/screens/LvDataCleanScreen.cpp @@ -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); diff --git a/src/ui/screens/LvSettingsScreen.cpp b/src/ui/screens/LvSettingsScreen.cpp index 0bb2672..b42aadb 100644 --- a/src/ui/screens/LvSettingsScreen.cpp +++ b/src/ui/screens/LvSettingsScreen.cpp @@ -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);