From 743500761a8a343a5389d6ab66889c5f3ec12606 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:24:46 +0200 Subject: [PATCH] feat(l2): light theme reworked - a tint for selected, grey values, quiet edges The light theme read as the dark one inverted. Now something selected (a tab, a segment, a tile, Fav) is a tint of the accent with the accent's text (SEL_*), the full accent fill stays for the primary action; setting values read in grey (VALUE); popups, the banner and the toast have a quiet grey edge (EDGE), the toast is a dark pill; neutral greys the RGB565 panel keeps neutral; the big clock graphite. Co-Authored-By: Claude Opus 5.5 --- .../companion_radio/ui-lvgl/ChannelScreen.h | 6 ++-- .../ui-lvgl/ConversationScreen.h | 5 +-- .../companion_radio/ui-lvgl/DeviceScreen.h | 2 +- examples/companion_radio/ui-lvgl/QuickPanel.h | 8 ++--- examples/companion_radio/ui-lvgl/Theme.h | 36 +++++++++++++++---- examples/companion_radio/ui-lvgl/UITask.cpp | 18 +++++----- examples/companion_radio/ui-lvgl/Widgets.h | 34 ++++++++++-------- 7 files changed, 71 insertions(+), 38 deletions(-) diff --git a/examples/companion_radio/ui-lvgl/ChannelScreen.h b/examples/companion_radio/ui-lvgl/ChannelScreen.h index 7b33ffa5..2967199d 100644 --- a/examples/companion_radio/ui-lvgl/ChannelScreen.h +++ b/examples/companion_radio/ui-lvgl/ChannelScreen.h @@ -100,9 +100,9 @@ void UITask::channelMenu(int idx) { }; for (auto& b : btns) { if (b.act == A_READ && !unread) continue; - lv_obj_t* bt = barButton(acts, b.text, onChanAction, b.act, b.act == A_FAV && chanctl::favourite(_prefs, idx)); + lv_obj_t* bt = barButton(acts, b.text, onChanAction, b.act); if (b.act == A_DELETE) s_del_lbl = lv_obj_get_child(bt, 0); - if (b.act == A_FAV) s_fav_btn = bt; + if (b.act == A_FAV) { s_fav_btn = bt; styleSelected(bt, chanctl::favourite(_prefs, idx)); } } } @@ -124,7 +124,7 @@ void UITask::channelAction(uint8_t act) { bool on = !chanctl::favourite(_prefs, idx); chanctl::setFavourite(_prefs, idx, on); prefsSave(); - if (s_fav_btn) barButtonOn(s_fav_btn, on); + if (s_fav_btn) styleSelected(s_fav_btn, on); showToast(on ? "Added to favourites" : "Removed from favourites", 1200); if (_screen == SCR_CHATS) { buildChats(); channelMenu(idx); } // list order / star break; diff --git a/examples/companion_radio/ui-lvgl/ConversationScreen.h b/examples/companion_radio/ui-lvgl/ConversationScreen.h index 25b53f58..eaf99a11 100644 --- a/examples/companion_radio/ui-lvgl/ConversationScreen.h +++ b/examples/companion_radio/ui-lvgl/ConversationScreen.h @@ -185,7 +185,8 @@ void UITask::conversationMenu(const uint8_t* pub_key) { } lv_obj_t* acts = buttonBar(panel); - s_fav_btn = barButton(acts, UI_SYMBOL_STAR " Fav", onConvAction, A_FAV, contactctl::favourite(ci)); + s_fav_btn = barButton(acts, UI_SYMBOL_STAR " Fav", onConvAction, A_FAV); + styleSelected(s_fav_btn, contactctl::favourite(ci)); if (!room && _core->dmUnread(ci.id.pub_key) > 0) barButton(acts, LV_SYMBOL_OK " Read", onConvAction, A_READ, false); barButton(acts, UI_SYMBOL_TACK, onConvAction, A_PIN, favslots::findContact(_prefs, ci.id.pub_key) >= 0); if (room) { @@ -208,7 +209,7 @@ void UITask::conversationAction(uint8_t act) { case A_FAV: { bool on = !contactctl::favourite(ci); if (!contactctl::setFavourite(ci.id.pub_key, on)) break; - if (s_fav_btn) barButtonOn(s_fav_btn, on); + if (s_fav_btn) styleSelected(s_fav_btn, on); showToast(on ? "Added to favourites" : "Removed from favourites", 1200); if (_screen == SCR_CHATS) { buildChats(); conversationMenu(s_key); } // star / filter break; diff --git a/examples/companion_radio/ui-lvgl/DeviceScreen.h b/examples/companion_radio/ui-lvgl/DeviceScreen.h index 165dcf37..20342a9d 100644 --- a/examples/companion_radio/ui-lvgl/DeviceScreen.h +++ b/examples/companion_radio/ui-lvgl/DeviceScreen.h @@ -419,7 +419,7 @@ void UITask::setAccent(int idx) { theme::setAccent(idx); lvport::saveAccent(theme::s_accent); theme::install(lv_display_get_default()); - if (_toast) lv_obj_set_style_border_color(_toast, lv_color_hex(theme::ACCENT), 0); + if (_toast) lv_obj_set_style_border_color(_toast, lv_color_hex(theme::EDGE), 0); _status_sig[0] = '\0'; // icons in the accent rebuilt refreshStatusBar(); pinRowRefresh(); // the page, kept at its bottom where this row is diff --git a/examples/companion_radio/ui-lvgl/QuickPanel.h b/examples/companion_radio/ui-lvgl/QuickPanel.h index 9a4bcef8..4ca766ac 100644 --- a/examples/companion_radio/ui-lvgl/QuickPanel.h +++ b/examples/companion_radio/ui-lvgl/QuickPanel.h @@ -99,15 +99,15 @@ static void onPagesScrolled(lv_event_t* e) { dotsSet(lv_obj_get_scroll_x(s_pages) > lv_obj_get_width(s_pages) / 2); } -// A tile's look: on in the accent with dark text, off on the surface; +// A tile's look: on in SEL_BG / SEL_FG (Theme.h), off on the surface; // `dim`: nothing to switch on this device. static void tileSet(int t, const char* icon, const char* cap, bool on, bool dim = false) { - const uint32_t fg = on ? theme::ON_ACCENT : dim ? theme::TEXT_MUTED : theme::TEXT; + const uint32_t fg = on ? theme::SEL_FG : dim ? theme::TEXT_MUTED : theme::TEXT; const uint8_t look = 1 | (on ? 2 : 0); if (s_look[t] != look) { // LVGL restyles, and redraws, on every set (the refresh is each second) s_look[t] = look; - lv_obj_set_style_bg_color(s_tile[t], lv_color_hex(on ? theme::ACCENT : theme::SURFACE), 0); - lv_obj_set_style_bg_color(s_tile[t], lv_color_hex(on ? theme::mix(theme::ACCENT, theme::BG, 75) : theme::SURFACE_2), LV_STATE_PRESSED); + lv_obj_set_style_bg_color(s_tile[t], lv_color_hex(on ? theme::SEL_BG : theme::SURFACE), 0); + lv_obj_set_style_bg_color(s_tile[t], lv_color_hex(on ? theme::SEL_PRESS : theme::SURFACE_2), LV_STATE_PRESSED); } setText(s_icon[t], icon); setText(s_cap[t], cap); diff --git a/examples/companion_radio/ui-lvgl/Theme.h b/examples/companion_radio/ui-lvgl/Theme.h index 3fa50fb4..499f0ded 100644 --- a/examples/companion_radio/ui-lvgl/Theme.h +++ b/examples/companion_radio/ui-lvgl/Theme.h @@ -14,7 +14,10 @@ // // Two themes, dark (the default) and light (Settings > Display > Theme). The // colours are set once, before anything is built (setLight()), so changing -// the theme restarts the device. +// the theme restarts the device. The light one isn't the dark one inverted: +// something selected is a tint of the accent with the accent's text (SEL_*), +// not a full accent fill -- that stays for the primary action and own +// bubbles -- values read in grey, and popups and banners have a quiet edge. #include // lv_theme_t's fields, to layer a theme namespace theme { @@ -28,6 +31,12 @@ namespace theme { static uint32_t TEXT_MUTED= 0x9A958C; static uint32_t OK = 0x6FCF6F; // delivered / relayed static uint32_t FAIL = 0xE5534B; // not delivered + // Set from those by setAccent(): + static uint32_t SEL_BG = 0xFFB000; // something selected / on: a tab, a segment, a tile + static uint32_t SEL_FG = 0x0B0B0D; // its text and icons + static uint32_t SEL_PRESS = 0xC08400; // it pressed + static uint32_t VALUE = 0xFFB000; // a setting's value (a choice, a typed value) + static uint32_t EDGE = 0xFFB000; // the edge of a popup, the banner, the toast static bool s_light = false; // Spacing (px) @@ -96,20 +105,34 @@ namespace theme { if (idx < 0 || idx >= ACCENT_COUNT) idx = 0; s_accent = idx; ACCENT = s_light ? ACCENTS[idx].light : ACCENTS[idx].col; + if (s_light) { + SEL_BG = mix(ACCENT, SURFACE, 20); + SEL_FG = mix(ACCENT, 0x000000, 78); + SEL_PRESS = mix(ACCENT, SURFACE, 32); + VALUE = TEXT_MUTED; + EDGE = SURFACE_2; + } else { + SEL_BG = ACCENT; + SEL_FG = ON_ACCENT; + SEL_PRESS = mix(ACCENT, BG, 75); + VALUE = ACCENT; + EDGE = ACCENT; + } } // Before anything is built (the colours are copied into what is). static void setLight(bool on) { s_light = on; if (on) { - BG = 0xF2F0EC; SURFACE = 0xFFFFFF; SURFACE_2 = 0xE2DFD9; - ON_ACCENT = 0xFFFFFF; TEXT = 0x1C1B19; TEXT_MUTED = 0x6E6A62; + // Neutral greys the panel's RGB565 keeps neutral (0xF3F3F1 came out pink). + BG = 0xEFEFEF; SURFACE = 0xFFFFFF; SURFACE_2 = 0xDEDEDE; + ON_ACCENT = 0xFFFFFF; TEXT = 0x1C1C1C; TEXT_MUTED = 0x6B6D6B; OK = 0x2E9A3E; FAIL = 0xD23A32; } setAccent(s_accent); } // Over LVGL's default theme, for every widget: a checked button is - // "selected" (the accent, ON_ACCENT text), buttons + // "selected" (SEL_BG, SEL_FG text), buttons // have the card radius and give a little under the finger (the default // theme's transitions animate it both ways). Scrollbars are thin and hug // the edge, inside the padding every panel and list has, instead of the @@ -137,7 +160,7 @@ namespace theme { lv_style_set_transform_width(&s_press, -2); lv_style_set_transform_height(&s_press, -2); lv_style_init(&s_checked); - lv_style_set_text_color(&s_checked, lv_color_hex(ON_ACCENT)); + lv_style_init(&s_sbar); lv_style_set_width(&s_sbar, 3); lv_style_set_pad_right(&s_sbar, 2); // vertical bar: 2 px off the edge @@ -145,7 +168,8 @@ namespace theme { lv_style_set_pad_top(&s_sbar, RADIUS / 2); lv_style_set_pad_left(&s_sbar, RADIUS / 2); } - lv_style_set_bg_color(&s_checked, lv_color_hex(ACCENT)); + lv_style_set_bg_color(&s_checked, lv_color_hex(SEL_BG)); + lv_style_set_text_color(&s_checked, lv_color_hex(SEL_FG)); lv_theme_t* base = lv_theme_default_init(disp, lv_color_hex(ACCENT), lv_color_hex(ACCENT), !s_light, THEME_FONT_BODY); s_theme = *base; lv_theme_set_parent(&s_theme, base); diff --git a/examples/companion_radio/ui-lvgl/UITask.cpp b/examples/companion_radio/ui-lvgl/UITask.cpp index a1a9867e..1e26e0b2 100644 --- a/examples/companion_radio/ui-lvgl/UITask.cpp +++ b/examples/companion_radio/ui-lvgl/UITask.cpp @@ -524,14 +524,15 @@ static void bannerBuild() { lv_obj_remove_flag(s_banner, LV_OBJ_FLAG_GESTURE_BUBBLE); lv_obj_set_size(s_banner, w - 12, LV_SIZE_CONTENT); lv_obj_align(s_banner, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 2); - lv_obj_set_style_bg_color(s_banner, lv_color_hex(theme::SURFACE_2), 0); - lv_obj_set_style_bg_color(s_banner, lv_color_hex(theme::SURFACE), LV_STATE_PRESSED); - lv_obj_set_style_border_color(s_banner, lv_color_hex(theme::ACCENT), 0); + // Raised off the page: lighter than it on the dark theme, white on the light. + lv_obj_set_style_bg_color(s_banner, lv_color_hex(theme::s_light ? theme::SURFACE : theme::SURFACE_2), 0); + lv_obj_set_style_bg_color(s_banner, lv_color_hex(theme::s_light ? theme::SURFACE_2 : theme::SURFACE), LV_STATE_PRESSED); + lv_obj_set_style_border_color(s_banner, lv_color_hex(theme::EDGE), 0); lv_obj_set_style_border_width(s_banner, 1, 0); lv_obj_set_style_radius(s_banner, theme::RADIUS, 0); lv_obj_set_style_shadow_width(s_banner, 12, 0); lv_obj_set_style_shadow_color(s_banner, lv_color_hex(0x000000), 0); - lv_obj_set_style_shadow_opa(s_banner, LV_OPA_60, 0); + lv_obj_set_style_shadow_opa(s_banner, theme::s_light ? LV_OPA_20 : LV_OPA_60, 0); lv_obj_set_style_pad_all(s_banner, 8, 0); lv_obj_set_style_pad_column(s_banner, 10, 0); lv_obj_set_flex_flow(s_banner, LV_FLEX_FLOW_ROW); @@ -644,14 +645,15 @@ void UITask::buildStatusBar() { lv_obj_remove_flag(_toast, LV_OBJ_FLAG_SCROLLABLE); lv_obj_remove_flag(_toast, LV_OBJ_FLAG_CLICKABLE); lv_obj_set_size(_toast, LV_SIZE_CONTENT, LV_SIZE_CONTENT); - lv_obj_set_style_bg_color(_toast, lv_color_hex(theme::SURFACE_2), 0); - lv_obj_set_style_border_color(_toast, lv_color_hex(theme::ACCENT), 0); - lv_obj_set_style_border_width(_toast, 1, 0); + // On the light theme a dark pill, as a phone's: a light one was lost on the page. + lv_obj_set_style_bg_color(_toast, lv_color_hex(theme::s_light ? theme::TEXT : theme::SURFACE_2), 0); + lv_obj_set_style_border_color(_toast, lv_color_hex(theme::EDGE), 0); + lv_obj_set_style_border_width(_toast, theme::s_light ? 0 : 1, 0); lv_obj_set_style_radius(_toast, theme::RADIUS, 0); lv_obj_set_style_pad_hor(_toast, 12, 0); lv_obj_set_style_pad_ver(_toast, 6, 0); lv_obj_align(_toast, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4); // slides down from under the status bar - lv_obj_t* tl = label(_toast, "", THEME_FONT_BODY, theme::TEXT); + lv_obj_t* tl = label(_toast, "", THEME_FONT_BODY, theme::s_light ? theme::SURFACE : theme::TEXT); lv_label_set_long_mode(tl, LV_LABEL_LONG_WRAP); // long texts wrap instead of running off the screen lv_obj_set_style_max_width(tl, lv_display_get_horizontal_resolution(NULL) - 40, 0); lv_obj_set_style_text_align(tl, LV_TEXT_ALIGN_CENTER, 0); diff --git a/examples/companion_radio/ui-lvgl/Widgets.h b/examples/companion_radio/ui-lvgl/Widgets.h index f7022282..58d05bde 100644 --- a/examples/companion_radio/ui-lvgl/Widgets.h +++ b/examples/companion_radio/ui-lvgl/Widgets.h @@ -346,7 +346,7 @@ static lv_obj_t* popupOpen(lv_obj_t* parent, PopFit fit, lv_obj_t*& overlay, int } if (fit != POP_FIT) lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_bg_color(panel, lv_color_hex(theme::BG), 0); - lv_obj_set_style_border_color(panel, lv_color_hex(theme::ACCENT), 0); + lv_obj_set_style_border_color(panel, lv_color_hex(theme::EDGE), 0); lv_obj_set_style_border_width(panel, 1, 0); lv_obj_set_style_radius(panel, theme::RADIUS, 0); lv_obj_set_style_pad_all(panel, theme::PAD, 0); @@ -855,7 +855,7 @@ static lv_obj_t* choiceCreate(lv_obj_t* parent, const char* opts, int sel, const Choice* ch = (Choice*)lv_malloc(sizeof(Choice)); memset(ch, 0, sizeof(*ch)); snprintf(ch->title, sizeof(ch->title), "%s", title ? title : ""); - ch->value = label(c, "", THEME_FONT_BODY, theme::ACCENT); + ch->value = label(c, "", THEME_FONT_BODY, theme::VALUE); lv_obj_set_style_max_width(ch->value, 140, 0); // a long value wraps (LONG_DOT mis-sizes with SIZE_CONTENT) lv_obj_set_style_text_align(ch->value, LV_TEXT_ALIGN_RIGHT, 0); label(c, LV_SYMBOL_DOWN, THEME_FONT_SMALL, theme::TEXT_MUTED); @@ -917,7 +917,7 @@ static lv_obj_t* rowValue(lv_obj_t* row, const char* text, lv_event_cb_t cb, voi lv_obj_add_flag(c, LV_OBJ_FLAG_CLICKABLE); lv_obj_set_ext_click_area(c, 10); lv_obj_set_style_opa(c, LV_OPA_60, LV_STATE_PRESSED); - label(c, text, THEME_FONT_BODY, theme::ACCENT); + label(c, text, THEME_FONT_BODY, theme::VALUE); label(c, LV_SYMBOL_EDIT, THEME_FONT_SMALL, theme::TEXT_MUTED); lv_obj_add_event_cb(c, cb, LV_EVENT_CLICKED, user); return c; @@ -970,9 +970,9 @@ static lv_obj_t* segmented(lv_obj_t* parent, const char** map, int sel, int w, i lv_obj_set_style_bg_opa(m, LV_OPA_TRANSP, 0); lv_obj_set_style_border_width(m, 0, 0); lv_obj_set_style_bg_color(m, lv_color_hex(item), LV_PART_ITEMS); - lv_obj_set_style_bg_color(m, lv_color_hex(theme::ACCENT), LV_PART_ITEMS | LV_STATE_CHECKED); + lv_obj_set_style_bg_color(m, lv_color_hex(theme::SEL_BG), LV_PART_ITEMS | LV_STATE_CHECKED); lv_obj_set_style_text_color(m, lv_color_hex(theme::TEXT), LV_PART_ITEMS); - lv_obj_set_style_text_color(m, lv_color_hex(theme::ON_ACCENT), LV_PART_ITEMS | LV_STATE_CHECKED); + lv_obj_set_style_text_color(m, lv_color_hex(theme::SEL_FG), LV_PART_ITEMS | LV_STATE_CHECKED); lv_obj_set_style_text_font(m, THEME_FONT_SMALL, LV_PART_ITEMS); lv_obj_set_style_shadow_width(m, 0, LV_PART_ITEMS); lv_obj_set_style_radius(m, theme::RADIUS, LV_PART_ITEMS); @@ -1024,8 +1024,8 @@ static void stylePrimary(lv_obj_t* b) { lv_obj_set_style_text_color(lv_obj_get_child(b, i), lv_color_hex(theme::ON_ACCENT), 0); } -// Something selected / on (a tab, a chip, a row): the accent with ON_ACCENT -// text, or back to the surface with TEXT. Every label under it follows. +// Something selected / on (a tab, a chip, a row): SEL_BG with SEL_FG text +// (Theme.h), or back to the surface with TEXT. Every label under it follows. static void textUnder(lv_obj_t* o, uint32_t col) { for (uint32_t i = 0; i < lv_obj_get_child_count(o); i++) { lv_obj_t* c = lv_obj_get_child(o, i); @@ -1034,14 +1034,15 @@ static void textUnder(lv_obj_t* o, uint32_t col) { } } static void styleSelected(lv_obj_t* b, bool on) { - lv_obj_set_style_bg_color(b, lv_color_hex(on ? theme::ACCENT : theme::SURFACE), 0); - lv_obj_set_style_bg_color(b, lv_color_hex(on ? theme::mix(theme::ACCENT, theme::BG, 75) : theme::SURFACE_2), LV_STATE_PRESSED); - textUnder(b, on ? theme::ON_ACCENT : theme::TEXT); + lv_obj_set_style_bg_color(b, lv_color_hex(on ? theme::SEL_BG : theme::SURFACE), 0); + lv_obj_set_style_bg_color(b, lv_color_hex(on ? theme::SEL_PRESS : theme::SURFACE_2), LV_STATE_PRESSED); + textUnder(b, on ? theme::SEL_FG : theme::TEXT); } // A row of equal buttons at the foot of a popup or screen: buttonBar(), then -// barButton() for each -- `accent` for the main action or one that's on -// (barButtonOn() flips it later). Returns the button; its label is child 0. +// barButton() for each -- `accent` for the main action, in the full accent +// (barButtonOn() flips it later; a button that is on, as Fav, takes +// styleSelected() instead). Returns the button; its label is child 0. static lv_obj_t* buttonBar(lv_obj_t* parent) { lv_obj_t* bar = lv_obj_create(parent); styleSurface(bar, theme::BG); @@ -1051,7 +1052,11 @@ static lv_obj_t* buttonBar(lv_obj_t* parent) { lv_obj_set_style_pad_column(bar, theme::GAP, 0); return bar; } -static void barButtonOn(lv_obj_t* b, bool on) { styleSelected(b, on); } +static void barButtonOn(lv_obj_t* b, bool on) { + lv_obj_set_style_bg_color(b, lv_color_hex(on ? theme::ACCENT : theme::SURFACE), 0); + lv_obj_set_style_bg_color(b, lv_color_hex(on ? theme::mix(theme::ACCENT, theme::BG, 75) : theme::SURFACE_2), LV_STATE_PRESSED); + textUnder(b, on ? theme::ON_ACCENT : theme::TEXT); +} static lv_obj_t* barButton(lv_obj_t* bar, const char* text, lv_event_cb_t cb, uintptr_t user, bool accent = false) { lv_obj_t* b = lv_button_create(bar); lv_obj_set_height(b, 40); @@ -1264,7 +1269,8 @@ static lv_obj_t* create(lv_obj_t* parent) { lv_obj_t* o = lv_obj_create(parent); lv_obj_remove_style_all(o); lv_obj_remove_flag(o, LV_OBJ_FLAG_CLICKABLE); - lv_obj_set_style_text_color(o, lv_color_hex(theme::TEXT), 0); + // Graphite on the light theme: that much black, that big, was heavy. + lv_obj_set_style_text_color(o, lv_color_hex(theme::s_light ? theme::mix(theme::TEXT, theme::BG, 80) : theme::TEXT), 0); Text* t = (Text*)lv_malloc(sizeof(Text)); if (t) { snprintf(t->hm, sizeof(t->hm), "--:--"); t->sec[0] = 0; } lv_obj_set_user_data(o, t);