Add dark color scheme, backlight timeout, and L/R swap feature
- Invert UI colors: black background with white text, yellow highlights - Add 30-second backlight timeout with auto-wake on button press - Add Swap L/R menu option with checkbox to swap audio channels - Update volume page styling (yellow bar, full-screen container) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
187
main/gui.c
187
main/gui.c
@@ -65,6 +65,8 @@ static void createBubble(lv_obj_t * scr);
|
||||
static void build_scrollable_menu(void);
|
||||
static void currentFocusIndex(menu_context_t *ctx);
|
||||
static lv_obj_t * addMenuItem(lv_obj_t *page, const char *text);
|
||||
static lv_obj_t * addMenuItemWithCheckbox(lv_obj_t *page, const char *text, bool checked, lv_obj_t **checkbox_out);
|
||||
static void update_checkbox_focus_style(lv_obj_t *cb, bool focused);
|
||||
static lv_obj_t* create_menu_container(void);
|
||||
static void show_bt_device_list(void);
|
||||
static void show_volume_control(void);
|
||||
@@ -103,6 +105,19 @@ static lv_obj_t *_currentPage = NULL;
|
||||
|
||||
static GuiMode_t _mode = GUI_BUBBLE;
|
||||
|
||||
// Backlight timeout
|
||||
#define BACKLIGHT_TIMEOUT_MS 30000
|
||||
static TickType_t _lastButtonPress = 0;
|
||||
static bool _backlightOn = true;
|
||||
|
||||
static void backlight_reset_timeout(void) {
|
||||
_lastButtonPress = xTaskGetTickCount();
|
||||
if (!_backlightOn) {
|
||||
gpio_set_level(PIN_NUM_BK_LIGHT, 1);
|
||||
_backlightOn = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Menu navigation stack
|
||||
#define MAX_MENU_STACK_SIZE 8
|
||||
typedef struct {
|
||||
@@ -137,6 +152,9 @@ static int _current_volume = 50; // Default volume (0-100)
|
||||
static lv_obj_t* _calibration_page = NULL;
|
||||
static lv_timer_t* _calibration_timer = NULL;
|
||||
|
||||
// Swap L/R checkbox
|
||||
static lv_obj_t* _swapLR_checkbox = NULL;
|
||||
|
||||
// Hide *all* headers/back buttons that LVGL may show for this menu
|
||||
static void menu_hide_headers(lv_obj_t *menu) {
|
||||
if (!menu) return;
|
||||
@@ -200,6 +218,7 @@ static lv_obj_t* create_main_page(void) {
|
||||
|
||||
addMenuItem(main_page, "Calibration");
|
||||
addMenuItem(main_page, "Volume");
|
||||
addMenuItemWithCheckbox(main_page, "Swap L/R", system_getSwapLR(), &_swapLR_checkbox);
|
||||
//addMenuItem(main_page, "About");
|
||||
addMenuItem(main_page, "Exit");
|
||||
|
||||
@@ -238,6 +257,7 @@ static void cleanup_menu(void) {
|
||||
_volume_page = NULL;
|
||||
_volume_bar = NULL;
|
||||
_volume_label = NULL;
|
||||
_swapLR_checkbox = NULL;
|
||||
if (_calibration_timer) {
|
||||
lv_timer_del(_calibration_timer);
|
||||
_calibration_timer = NULL;
|
||||
@@ -257,7 +277,9 @@ static void create_lvgl_demo(void)
|
||||
lvgl_port_lock(0);
|
||||
// Create a screen with black background
|
||||
lv_obj_t *scr = lv_scr_act();
|
||||
|
||||
lv_obj_set_style_bg_color(scr, lv_color_black(), 0);
|
||||
lv_obj_set_style_bg_opa(scr, LV_OPA_COVER, 0);
|
||||
|
||||
createBubble(scr);
|
||||
// Menu will be created lazily when needed
|
||||
|
||||
@@ -462,6 +484,14 @@ static void btn_click_cb(lv_event_t * e) {
|
||||
menu_stack_push(_currentPage);
|
||||
show_volume_control();
|
||||
UNLOCK();
|
||||
} else if (strcmp(txt, "Swap L/R") == 0) {
|
||||
// Toggle the swap L/R state
|
||||
system_toggleSwapLR();
|
||||
// Update the checkbox visual (now a label showing "X" or empty)
|
||||
if (_swapLR_checkbox) {
|
||||
lv_label_set_text(_swapLR_checkbox, system_getSwapLR() ? "X" : "");
|
||||
}
|
||||
ESP_LOGI(TAG, "Swap L/R toggled to: %s", system_getSwapLR() ? "ON" : "OFF");
|
||||
} else if (strcmp(txt, "Back") == 0) {
|
||||
LOCK();
|
||||
menu_go_back();
|
||||
@@ -498,18 +528,18 @@ static void refresh_highlight(void) {
|
||||
|
||||
if (lv_obj_has_flag(child, LV_OBJ_FLAG_USER_1))
|
||||
{
|
||||
lv_obj_set_style_bg_color(child, lv_color_hex(0xFF8800), 0);
|
||||
lv_obj_set_style_bg_color(child, lv_color_hex(0xFFFF00), 0);
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(child,0),
|
||||
lv_color_white(), 0);
|
||||
lv_color_black(), 0);
|
||||
|
||||
selected = child;
|
||||
ESP_LOGI(TAG, "Selected");
|
||||
}
|
||||
else
|
||||
else
|
||||
{
|
||||
lv_obj_set_style_bg_color(child, lv_color_white(), 0);
|
||||
lv_obj_set_style_bg_color(child, lv_color_black(), 0);
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(child,0),
|
||||
lv_color_black(), 0);
|
||||
lv_color_white(), 0);
|
||||
}
|
||||
|
||||
index++;
|
||||
@@ -560,6 +590,12 @@ static void menuInc(int inc)
|
||||
// Update scroll on old focused item (stop scrolling)
|
||||
update_label_scroll(_menuContext.obj, false);
|
||||
|
||||
// Update checkbox style on old focused item (if it has one)
|
||||
lv_obj_t *old_cb = lv_obj_get_user_data(_menuContext.obj);
|
||||
if (old_cb) {
|
||||
update_checkbox_focus_style(old_cb, false);
|
||||
}
|
||||
|
||||
lv_obj_add_state(next, LV_STATE_FOCUSED);
|
||||
lv_obj_clear_state(_menuContext.obj, LV_STATE_FOCUSED);
|
||||
lv_obj_scroll_to_view(next, LV_ANIM_ON);
|
||||
@@ -567,6 +603,12 @@ static void menuInc(int inc)
|
||||
// Update scroll on new focused item (start scrolling)
|
||||
update_label_scroll(next, true);
|
||||
|
||||
// Update checkbox style on new focused item (if it has one)
|
||||
lv_obj_t *new_cb = lv_obj_get_user_data(next);
|
||||
if (new_cb) {
|
||||
update_checkbox_focus_style(new_cb, true);
|
||||
}
|
||||
|
||||
_menuContext.obj = next;
|
||||
_menuContext.selected = search_index;
|
||||
}
|
||||
@@ -626,6 +668,83 @@ static void label_scroll_focus_cb(lv_event_t * e)
|
||||
}
|
||||
}
|
||||
|
||||
static lv_obj_t * addMenuItemWithCheckbox(lv_obj_t *page, const char *text, bool checked, lv_obj_t **checkbox_out)
|
||||
{
|
||||
if (!page || !text) {
|
||||
ESP_LOGE(TAG, "Null parameters in addMenuItemWithCheckbox");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Ensure styles are initialized
|
||||
ensure_menu_styles();
|
||||
|
||||
lv_obj_t * btn = lv_btn_create(page);
|
||||
if (!btn) {
|
||||
ESP_LOGE(TAG, "Failed to create button in addMenuItemWithCheckbox");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
lv_obj_set_size(btn, LV_PCT(100), ROW_H);
|
||||
|
||||
lv_obj_add_style(btn, &_styleUnfocusedBtn, LV_PART_MAIN | LV_STATE_DEFAULT);
|
||||
lv_obj_add_style(btn, &_styleFocusedBtn, LV_PART_MAIN | LV_STATE_FOCUSED);
|
||||
|
||||
lv_obj_add_state(btn, LV_STATE_DEFAULT);
|
||||
|
||||
lv_obj_set_style_radius(btn, 0, LV_PART_MAIN);
|
||||
lv_obj_set_style_border_width(btn, 0, LV_PART_MAIN);
|
||||
lv_obj_clear_flag(btn, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
// Label on the left (same as regular menu item)
|
||||
lv_obj_t * lbl = lv_label_create(btn);
|
||||
if (!lbl) {
|
||||
ESP_LOGE(TAG, "Failed to create label in addMenuItemWithCheckbox");
|
||||
lv_obj_del(btn);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
lv_label_set_text(lbl, text);
|
||||
lv_label_set_long_mode(lbl, LV_LABEL_LONG_CLIP);
|
||||
lv_obj_set_width(lbl, LV_PCT(80));
|
||||
lv_obj_set_style_radius(lbl, 0, LV_PART_MAIN | LV_STATE_ANY);
|
||||
lv_obj_set_style_border_width(lbl, 0, LV_PART_MAIN | LV_STATE_ANY);
|
||||
lv_obj_align(lbl, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
// Label on the right to act as checkbox (shows "X" when checked, empty when not)
|
||||
lv_obj_t * cb = lv_label_create(btn);
|
||||
lv_label_set_text(cb, checked ? "X" : "");
|
||||
lv_obj_set_style_text_color(cb, lv_color_white(), 0); // Unfocused: white
|
||||
lv_obj_align(cb, LV_ALIGN_RIGHT_MID, -4, 0);
|
||||
|
||||
if (checkbox_out) {
|
||||
*checkbox_out = cb;
|
||||
}
|
||||
|
||||
// Add focus event handler to control scrolling
|
||||
lv_obj_add_event_cb(btn, label_scroll_focus_cb, LV_EVENT_ALL, NULL);
|
||||
|
||||
// click callback
|
||||
lv_obj_add_event_cb(btn, btn_click_cb, LV_EVENT_CLICKED, NULL);
|
||||
|
||||
// Store checkbox in user data for focus styling updates
|
||||
lv_obj_set_user_data(btn, cb);
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
// Update checkbox label color based on parent focus state
|
||||
static void update_checkbox_focus_style(lv_obj_t *cb, bool focused) {
|
||||
if (!cb) return;
|
||||
|
||||
if (focused) {
|
||||
// Focused: black text (on yellow background)
|
||||
lv_obj_set_style_text_color(cb, lv_color_black(), 0);
|
||||
} else {
|
||||
// Unfocused: white text (on black background)
|
||||
lv_obj_set_style_text_color(cb, lv_color_white(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
static lv_obj_t * addMenuItem(lv_obj_t *page, const char *text)
|
||||
{
|
||||
if (!page || !text) {
|
||||
@@ -719,12 +838,12 @@ static void ensure_menu_styles(void) {
|
||||
lv_style_init(&_styleFocusedBtn);
|
||||
lv_style_init(&_styleUnfocusedBtn);
|
||||
|
||||
lv_style_set_bg_color(&_styleUnfocusedBtn, lv_color_make(0xff,0xff,0xff)); // white bg
|
||||
lv_style_set_text_color(&_styleUnfocusedBtn, lv_color_black()); // black text
|
||||
lv_style_set_bg_color(&_styleUnfocusedBtn, lv_color_make(0x00,0x00,0x00)); // black bg
|
||||
lv_style_set_text_color(&_styleUnfocusedBtn, lv_color_white()); // white text
|
||||
//lv_style_set_text_font(&_styleUnfocusedBtn, &lv_font_unscii_16); // larger font
|
||||
|
||||
lv_style_set_bg_color(&_styleFocusedBtn, lv_color_make(0x33,0x99,0xFF)); // blue bg
|
||||
lv_style_set_text_color(&_styleFocusedBtn, lv_color_white()); // white text
|
||||
lv_style_set_bg_color(&_styleFocusedBtn, lv_color_make(0xFF,0xFF,0x00)); // bright yellow bg
|
||||
lv_style_set_text_color(&_styleFocusedBtn, lv_color_black()); // black text
|
||||
//lv_style_set_text_font(&_styleFocusedBtn, &lv_font_unscii_16); // larger font
|
||||
|
||||
styles_initialized = true;
|
||||
@@ -1066,9 +1185,9 @@ static lv_obj_t* create_bt_device_page(bool start_discovery) {
|
||||
lv_label_set_text(_bt_status_item, "Devices:");
|
||||
lv_obj_set_width(_bt_status_item, lv_pct(100));
|
||||
lv_obj_set_height(_bt_status_item, 16); // Thinner header
|
||||
lv_obj_set_style_bg_color(_bt_status_item, lv_color_make(0xE0, 0xE0, 0xE0), 0);
|
||||
lv_obj_set_style_bg_color(_bt_status_item, lv_color_make(0x00, 0x00, 0x00), 0);
|
||||
lv_obj_set_style_bg_opa(_bt_status_item, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_text_color(_bt_status_item, lv_color_black(), 0);
|
||||
lv_obj_set_style_text_color(_bt_status_item, lv_color_white(), 0);
|
||||
lv_obj_set_style_pad_left(_bt_status_item, 5, 0);
|
||||
lv_obj_set_style_pad_top(_bt_status_item, 2, 0);
|
||||
lv_obj_set_style_text_align(_bt_status_item, LV_TEXT_ALIGN_LEFT, 0);
|
||||
@@ -1086,7 +1205,7 @@ static lv_obj_t* create_bt_device_page(bool start_discovery) {
|
||||
lv_obj_set_style_outline_width(_bt_device_container, 0, LV_PART_MAIN);
|
||||
lv_obj_set_style_pad_all(_bt_device_container, 0, LV_PART_MAIN);
|
||||
lv_obj_set_style_pad_row(_bt_device_container, 0, LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(_bt_device_container, lv_color_white(), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_color(_bt_device_container, lv_color_black(), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_opa(_bt_device_container, LV_OPA_COVER, LV_PART_MAIN);
|
||||
lv_obj_set_flex_flow(_bt_device_container, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_scrollbar_mode(_bt_device_container, LV_SCROLLBAR_MODE_AUTO);
|
||||
@@ -1235,9 +1354,11 @@ static lv_obj_t* create_volume_page(void) {
|
||||
|
||||
// Create a container to center everything vertically
|
||||
lv_obj_t* container = lv_obj_create(_volume_page);
|
||||
lv_obj_set_size(container, lv_pct(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_bg_opa(container, LV_OPA_TRANSP, 0);
|
||||
lv_obj_set_size(container, lv_pct(100), lv_pct(100));
|
||||
lv_obj_set_style_bg_color(container, lv_color_black(), 0);
|
||||
lv_obj_set_style_bg_opa(container, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(container, 0, 0);
|
||||
lv_obj_set_style_radius(container, 0, 0);
|
||||
lv_obj_set_style_pad_all(container, 0, 0);
|
||||
lv_obj_set_flex_flow(container, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(container, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
@@ -1247,7 +1368,7 @@ static lv_obj_t* create_volume_page(void) {
|
||||
lv_obj_t* title = lv_label_create(container);
|
||||
lv_label_set_text(title, "Volume Control");
|
||||
lv_obj_set_style_text_align(title, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_set_style_text_color(title, lv_color_black(), 0);
|
||||
lv_obj_set_style_text_color(title, lv_color_white(), 0);
|
||||
lv_obj_set_style_pad_bottom(title, 8, 0);
|
||||
|
||||
// Create volume bar (progress bar)
|
||||
@@ -1255,6 +1376,8 @@ static lv_obj_t* create_volume_page(void) {
|
||||
lv_obj_set_size(_volume_bar, 120, 20);
|
||||
lv_bar_set_range(_volume_bar, 0, 100);
|
||||
lv_bar_set_value(_volume_bar, _current_volume, LV_ANIM_OFF);
|
||||
lv_obj_set_style_bg_color(_volume_bar, lv_color_make(0x40, 0x40, 0x40), LV_PART_MAIN); // Dark gray background
|
||||
lv_obj_set_style_bg_color(_volume_bar, lv_color_make(0xFF, 0xFF, 0x00), LV_PART_INDICATOR); // Yellow indicator
|
||||
lv_obj_set_style_pad_top(_volume_bar, 5, 0);
|
||||
lv_obj_set_style_pad_bottom(_volume_bar, 5, 0);
|
||||
|
||||
@@ -1264,6 +1387,7 @@ static lv_obj_t* create_volume_page(void) {
|
||||
snprintf(volume_text, sizeof(volume_text), "%d%%", _current_volume);
|
||||
lv_label_set_text(_volume_label, volume_text);
|
||||
lv_obj_set_style_text_align(_volume_label, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_set_style_text_color(_volume_label, lv_color_white(), 0);
|
||||
lv_obj_set_style_pad_top(_volume_label, 5, 0);
|
||||
|
||||
return _volume_page;
|
||||
@@ -1438,7 +1562,7 @@ static void calibration_click_cb(lv_event_t * e) {
|
||||
// Show "Calibrated" message centered
|
||||
lv_obj_t* label = lv_label_create(container);
|
||||
lv_label_set_text(label, "Calibrated");
|
||||
lv_obj_set_style_text_color(label, lv_color_black(), 0);
|
||||
lv_obj_set_style_text_color(label, lv_color_white(), 0);
|
||||
lv_obj_set_style_text_font(label, &lv_font_montserrat_14, 0);
|
||||
lv_obj_center(label);
|
||||
}
|
||||
@@ -1467,7 +1591,7 @@ static void calibration_click_cb(lv_event_t * e) {
|
||||
// Show "Cleared" message centered
|
||||
lv_obj_t* label = lv_label_create(container);
|
||||
lv_label_set_text(label, "Cleared");
|
||||
lv_obj_set_style_text_color(label, lv_color_black(), 0);
|
||||
lv_obj_set_style_text_color(label, lv_color_white(), 0);
|
||||
lv_obj_set_style_text_font(label, &lv_font_montserrat_14, 0);
|
||||
lv_obj_center(label);
|
||||
}
|
||||
@@ -1556,11 +1680,11 @@ static void build_scrollable_menu(void) {
|
||||
lv_style_init(&_styleFocusedBtn);
|
||||
lv_style_init(&_styleUnfocusedBtn);
|
||||
|
||||
lv_style_set_bg_color(&_styleUnfocusedBtn, lv_color_make(0xff,0xff,0xff)); // gray bg
|
||||
lv_style_set_text_color(&_styleUnfocusedBtn, lv_color_hex(0xFF8800));
|
||||
lv_style_set_bg_color(&_styleUnfocusedBtn, lv_color_make(0x00,0x00,0x00)); // black bg
|
||||
lv_style_set_text_color(&_styleUnfocusedBtn, lv_color_white());
|
||||
|
||||
lv_style_set_bg_color(&_styleFocusedBtn, lv_color_make(0x33,0x99,0xFF)); // blue bg
|
||||
lv_style_set_text_color(&_styleUnfocusedBtn,lv_color_black());
|
||||
lv_style_set_bg_color(&_styleFocusedBtn, lv_color_make(0xFF,0xFF,0x00)); // bright yellow bg
|
||||
lv_style_set_text_color(&_styleFocusedBtn, lv_color_black());
|
||||
|
||||
|
||||
// 2) Inside it, create the lv_menu and hide its sidebar/header
|
||||
@@ -1653,13 +1777,28 @@ static void gui_task(void *pvParameters)
|
||||
lv_obj_remove_flag(_bubble, LV_OBJ_FLAG_HIDDEN);
|
||||
UNLOCK();
|
||||
|
||||
// Initialize backlight timeout
|
||||
_lastButtonPress = xTaskGetTickCount();
|
||||
_backlightOn = true;
|
||||
|
||||
ESP_LOGI(TAG, "Start GUI Task...");
|
||||
while (1)
|
||||
{
|
||||
|
||||
// Check backlight timeout
|
||||
if (_backlightOn) {
|
||||
TickType_t elapsed = xTaskGetTickCount() - _lastButtonPress;
|
||||
if (elapsed > pdMS_TO_TICKS(BACKLIGHT_TIMEOUT_MS)) {
|
||||
gpio_set_level(PIN_NUM_BK_LIGHT, 0);
|
||||
_backlightOn = false;
|
||||
ESP_LOGI(TAG, "Backlight off due to timeout");
|
||||
}
|
||||
}
|
||||
|
||||
if (xQueueReceive(q, &ev, pdMS_TO_TICKS(10)) == pdTRUE)
|
||||
if (xQueueReceive(q, &ev, pdMS_TO_TICKS(10)) == pdTRUE)
|
||||
{
|
||||
// Reset backlight timeout on any button press
|
||||
backlight_reset_timeout();
|
||||
|
||||
switch (ev) {
|
||||
#if 0
|
||||
case (KEY_UP << KEY_LONG_PRESS):
|
||||
|
||||
Reference in New Issue
Block a user