#include "ui_mixer.h" #include "lcd.h" #include "lvgl.h" #include "src/draw/sw/lv_draw_sw_utils.h" #include "pico/stdlib.h" #include #include #define UI_HEADER_H 56 #define UI_LIST_TOP 58 #define UI_ROW_H 45 #define UI_MAX_ROWS 4 #define UI_BUF_LINES 30 #define UI_SB_W 8 #define UI_SB_PAD 4 #define UI_CONTENT_R (UI_SB_W + UI_SB_PAD + 2) /* right inset for list content */ #define COL_BG lv_color_make(10, 14, 20) #define COL_PANEL lv_color_make(22, 30, 42) #define COL_SEL lv_color_make(28, 48, 72) #define COL_ACCENT lv_color_make(0, 200, 210) #define COL_ACCENT2 lv_color_make(255, 170, 40) #define COL_MUTED lv_color_make(120, 130, 145) #define COL_BAR_BG lv_color_make(40, 52, 68) #define COL_BAR_HI lv_color_make(80, 230, 170) #define COL_VU lv_color_make(255, 200, 60) #define COL_VU_HOT lv_color_make(255, 90, 70) #define COL_TEXT lv_color_make(235, 240, 245) typedef struct { lv_obj_t *row; lv_obj_t *name; lv_obj_t *pct; lv_obj_t *vu; lv_obj_t *bar; } ui_row_t; static lv_display_t *s_disp; static uint8_t s_draw_buf[LCD_WIDTH * UI_BUF_LINES * 2]; static lv_obj_t *s_scr; static lv_obj_t *s_wait; static lv_obj_t *s_main; static lv_obj_t *s_master_pct; static lv_obj_t *s_master_vu; static lv_obj_t *s_master_bar; static lv_obj_t *s_empty; static lv_obj_t *s_caret; static lv_obj_t *s_sb_track; static lv_obj_t *s_sb_thumb; static ui_row_t s_rows[UI_MAX_ROWS]; static bool s_waiting = true; static uint8_t s_master = 0xFF; static uint8_t s_master_peak = 0xFF; static uint8_t s_n = 0xFF; static uint8_t s_sel = 0xFF; static uint8_t s_page = 0xFF; static uint8_t s_count = 0xFF; static char s_names[AUSIO_MAX_VISIBLE][AUSIO_NAME_CHARS + 1]; static uint8_t s_vols[AUSIO_MAX_VISIBLE]; static uint8_t s_peaks[AUSIO_MAX_VISIBLE]; static absolute_time_t s_last_tick; static int16_t s_caret_y; static void flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) { int32_t w = area->x2 - area->x1 + 1; int32_t h = area->y2 - area->y1 + 1; uint32_t stride = lv_draw_buf_width_to_stride((uint32_t)w, lv_display_get_color_format(disp)); /* SPI wants big-endian RGB565 bytes (same as lcd_draw_char). */ if (stride == (uint32_t)w * 2u) { lv_draw_sw_rgb565_swap(px_map, (uint32_t)(w * h)); } else { for (int32_t row = 0; row < h; row++) { lv_draw_sw_rgb565_swap(px_map + (size_t)row * stride, (uint32_t)w); } } lcd_blit((int16_t)area->x1, (int16_t)area->y1, (int16_t)w, (int16_t)h, px_map, (int32_t)stride); lv_display_flush_ready(disp); } static void style_bar(lv_obj_t *bar, lv_color_t fill) { lv_obj_set_style_bg_color(bar, COL_BAR_BG, LV_PART_MAIN); lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, LV_PART_MAIN); lv_obj_set_style_radius(bar, 4, LV_PART_MAIN); lv_obj_set_style_pad_all(bar, 0, LV_PART_MAIN); lv_obj_set_style_bg_color(bar, fill, LV_PART_INDICATOR); lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, LV_PART_INDICATOR); lv_obj_set_style_radius(bar, 4, LV_PART_INDICATOR); lv_bar_set_range(bar, 0, 100); } static void style_vu(lv_obj_t *bar) { style_bar(bar, COL_VU); lv_obj_set_style_radius(bar, 2, LV_PART_MAIN); lv_obj_set_style_radius(bar, 2, LV_PART_INDICATOR); } static void set_vu_value(lv_obj_t *bar, uint8_t peak) { lv_color_t c = (peak >= 90) ? COL_VU_HOT : COL_VU; lv_obj_set_style_bg_color(bar, c, LV_PART_INDICATOR); lv_bar_set_value(bar, peak, LV_ANIM_OFF); } static void anim_caret_y(void *obj, int32_t v) { lv_obj_set_y((lv_obj_t *)obj, (int32_t)v); s_caret_y = (int16_t)v; } static void move_caret_to(uint8_t row_idx, bool animate) { int16_t target = (int16_t)(UI_LIST_TOP + (int16_t)row_idx * UI_ROW_H + 8); if (!animate) { lv_anim_delete(s_caret, anim_caret_y); lv_obj_set_y(s_caret, target); s_caret_y = target; return; } lv_anim_t a; lv_anim_init(&a); lv_anim_set_var(&a, s_caret); lv_anim_set_values(&a, s_caret_y, target); lv_anim_set_duration(&a, 180); lv_anim_set_path_cb(&a, lv_anim_path_ease_out); lv_anim_set_exec_cb(&a, anim_caret_y); lv_anim_start(&a); } static void update_scrollbar(uint8_t total, uint8_t page_start, uint8_t visible) { if (total <= visible || visible == 0) { lv_obj_add_flag(s_sb_track, LV_OBJ_FLAG_HIDDEN); return; } lv_obj_clear_flag(s_sb_track, LV_OBJ_FLAG_HIDDEN); const int32_t track_h = lv_obj_get_height(s_sb_track); /* Thumb shows the visible window; at least ~1/4 of track so it's obvious. */ int32_t thumb_h = (track_h * (int32_t)visible) / (int32_t)total; int32_t min_h = track_h / 4; if (min_h < 18) { min_h = 18; } if (thumb_h < min_h) { thumb_h = min_h; } if (thumb_h > track_h) { thumb_h = track_h; } uint8_t max_start = (uint8_t)(total - visible); int32_t travel = track_h - thumb_h; int32_t thumb_y = 0; if (max_start > 0 && travel > 0) { if (page_start > max_start) { page_start = max_start; } thumb_y = (travel * (int32_t)page_start) / (int32_t)max_start; } lv_obj_set_height(s_sb_thumb, thumb_h); lv_obj_set_y(s_sb_thumb, thumb_y); } static void build_wait_screen(void) { s_wait = lv_obj_create(s_scr); lv_obj_set_size(s_wait, LCD_WIDTH, LCD_HEIGHT); lv_obj_set_style_bg_color(s_wait, COL_BG, 0); lv_obj_set_style_bg_opa(s_wait, LV_OPA_COVER, 0); lv_obj_set_style_border_width(s_wait, 0, 0); lv_obj_set_style_pad_all(s_wait, 0, 0); lv_obj_set_style_radius(s_wait, 0, 0); lv_obj_clear_flag(s_wait, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *top = lv_obj_create(s_wait); lv_obj_set_size(top, LCD_WIDTH, 3); lv_obj_set_pos(top, 0, 0); lv_obj_set_style_bg_color(top, COL_ACCENT, 0); lv_obj_set_style_bg_opa(top, LV_OPA_COVER, 0); lv_obj_set_style_border_width(top, 0, 0); lv_obj_set_style_radius(top, 0, 0); lv_obj_clear_flag(top, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *spin = lv_spinner_create(s_wait); lv_obj_set_size(spin, 42, 42); lv_obj_align(spin, LV_ALIGN_CENTER, 0, -28); lv_obj_set_style_arc_color(spin, COL_PANEL, LV_PART_MAIN); lv_obj_set_style_arc_width(spin, 4, LV_PART_MAIN); lv_obj_set_style_arc_color(spin, COL_ACCENT, LV_PART_INDICATOR); lv_obj_set_style_arc_width(spin, 4, LV_PART_INDICATOR); lv_obj_t *l1 = lv_label_create(s_wait); lv_label_set_text(l1, "Waiting for host"); lv_obj_set_style_text_color(l1, COL_MUTED, 0); lv_obj_set_style_text_font(l1, &lv_font_montserrat_16, 0); lv_obj_align(l1, LV_ALIGN_CENTER, 0, 28); lv_obj_t *l2 = lv_label_create(s_wait); lv_label_set_text(l2, "Ausiomixer"); lv_obj_set_style_text_color(l2, COL_ACCENT, 0); lv_obj_set_style_text_font(l2, &lv_font_montserrat_20, 0); lv_obj_align(l2, LV_ALIGN_CENTER, 0, 54); } static void build_main_screen(void) { s_main = lv_obj_create(s_scr); lv_obj_set_size(s_main, LCD_WIDTH, LCD_HEIGHT); lv_obj_set_style_bg_color(s_main, COL_BG, 0); lv_obj_set_style_bg_opa(s_main, LV_OPA_COVER, 0); lv_obj_set_style_border_width(s_main, 0, 0); lv_obj_set_style_pad_all(s_main, 0, 0); lv_obj_set_style_radius(s_main, 0, 0); lv_obj_clear_flag(s_main, LV_OBJ_FLAG_SCROLLABLE); lv_obj_add_flag(s_main, LV_OBJ_FLAG_HIDDEN); lv_obj_t *header = lv_obj_create(s_main); lv_obj_set_size(header, LCD_WIDTH, UI_HEADER_H); lv_obj_set_pos(header, 0, 0); lv_obj_set_style_bg_color(header, COL_PANEL, 0); lv_obj_set_style_bg_opa(header, LV_OPA_COVER, 0); lv_obj_set_style_border_width(header, 0, 0); lv_obj_set_style_pad_all(header, 0, 0); lv_obj_set_style_radius(header, 0, 0); lv_obj_clear_flag(header, LV_OBJ_FLAG_SCROLLABLE); lv_obj_t *accent = lv_obj_create(header); lv_obj_set_size(accent, LCD_WIDTH, 2); lv_obj_set_pos(accent, 0, 0); lv_obj_set_style_bg_color(accent, COL_ACCENT, 0); lv_obj_set_style_bg_opa(accent, LV_OPA_COVER, 0); lv_obj_set_style_border_width(accent, 0, 0); lv_obj_set_style_radius(accent, 0, 0); lv_obj_t *ml = lv_label_create(header); lv_label_set_text(ml, "MASTER"); lv_obj_set_style_text_color(ml, COL_ACCENT, 0); lv_obj_set_style_text_font(ml, &lv_font_montserrat_14, 0); lv_obj_set_pos(ml, 10, 8); s_master_pct = lv_label_create(header); lv_label_set_text(s_master_pct, "0%"); lv_obj_set_style_text_color(s_master_pct, COL_TEXT, 0); lv_obj_set_style_text_font(s_master_pct, &lv_font_montserrat_28, 0); lv_obj_align(s_master_pct, LV_ALIGN_TOP_RIGHT, -10, 2); s_master_vu = lv_bar_create(header); lv_obj_set_size(s_master_vu, LCD_WIDTH - 20, 5); lv_obj_set_pos(s_master_vu, 10, 30); style_vu(s_master_vu); s_master_bar = lv_bar_create(header); lv_obj_set_size(s_master_bar, LCD_WIDTH - 20, 12); lv_obj_set_pos(s_master_bar, 10, 38); style_bar(s_master_bar, COL_BAR_HI); for (uint8_t i = 0; i < UI_MAX_ROWS; i++) { int16_t y = (int16_t)(UI_LIST_TOP + (int16_t)i * UI_ROW_H); ui_row_t *r = &s_rows[i]; r->row = lv_obj_create(s_main); lv_obj_set_size(r->row, LCD_WIDTH, UI_ROW_H - 1); lv_obj_set_pos(r->row, 0, y); lv_obj_set_style_bg_color(r->row, COL_BG, 0); lv_obj_set_style_bg_opa(r->row, LV_OPA_COVER, 0); lv_obj_set_style_border_width(r->row, 0, 0); lv_obj_set_style_pad_all(r->row, 0, 0); lv_obj_set_style_radius(r->row, 0, 0); lv_obj_clear_flag(r->row, LV_OBJ_FLAG_SCROLLABLE); lv_obj_add_flag(r->row, LV_OBJ_FLAG_HIDDEN); r->name = lv_label_create(r->row); lv_label_set_long_mode(r->name, LV_LABEL_LONG_SCROLL_CIRCULAR); lv_obj_set_width(r->name, LCD_WIDTH - 14 - 64 - UI_CONTENT_R); lv_obj_set_style_text_color(r->name, COL_ACCENT2, 0); lv_obj_set_style_text_font(r->name, &lv_font_montserrat_16, 0); lv_obj_set_pos(r->name, 14, 2); lv_label_set_text(r->name, ""); r->pct = lv_label_create(r->row); lv_label_set_text(r->pct, "0%"); lv_obj_set_style_text_color(r->pct, COL_MUTED, 0); lv_obj_set_style_text_font(r->pct, &lv_font_montserrat_14, 0); lv_obj_align(r->pct, LV_ALIGN_TOP_RIGHT, -(10 + UI_CONTENT_R), 4); r->vu = lv_bar_create(r->row); lv_obj_set_size(r->vu, LCD_WIDTH - 28 - UI_CONTENT_R, 5); lv_obj_set_pos(r->vu, 14, 22); style_vu(r->vu); r->bar = lv_bar_create(r->row); lv_obj_set_size(r->bar, LCD_WIDTH - 28 - UI_CONTENT_R, 9); lv_obj_set_pos(r->bar, 14, 30); style_bar(r->bar, COL_BAR_HI); } s_empty = lv_label_create(s_main); lv_label_set_text(s_empty, "No audio apps"); lv_obj_set_style_text_color(s_empty, COL_MUTED, 0); lv_obj_set_style_text_font(s_empty, &lv_font_montserrat_16, 0); lv_obj_align(s_empty, LV_ALIGN_CENTER, 0, 20); lv_obj_add_flag(s_empty, LV_OBJ_FLAG_HIDDEN); s_caret = lv_obj_create(s_main); lv_obj_set_size(s_caret, 4, UI_ROW_H - 16); lv_obj_set_pos(s_caret, 0, UI_LIST_TOP + 8); lv_obj_set_style_bg_color(s_caret, COL_ACCENT, 0); lv_obj_set_style_bg_opa(s_caret, LV_OPA_COVER, 0); lv_obj_set_style_border_width(s_caret, 0, 0); lv_obj_set_style_radius(s_caret, 2, 0); lv_obj_clear_flag(s_caret, LV_OBJ_FLAG_SCROLLABLE); lv_obj_add_flag(s_caret, LV_OBJ_FLAG_HIDDEN); s_caret_y = UI_LIST_TOP + 8; /* List scrollbar — visible only when session_count > visible rows. */ { const int16_t list_h = (int16_t)(UI_MAX_ROWS * UI_ROW_H); s_sb_track = lv_obj_create(s_main); lv_obj_set_size(s_sb_track, UI_SB_W, list_h - 12); lv_obj_set_pos(s_sb_track, LCD_WIDTH - UI_SB_W - UI_SB_PAD, UI_LIST_TOP + 6); lv_obj_set_style_bg_color(s_sb_track, COL_BAR_BG, 0); lv_obj_set_style_bg_opa(s_sb_track, LV_OPA_COVER, 0); lv_obj_set_style_border_width(s_sb_track, 0, 0); lv_obj_set_style_radius(s_sb_track, 4, 0); lv_obj_set_style_pad_all(s_sb_track, 0, 0); lv_obj_clear_flag(s_sb_track, LV_OBJ_FLAG_SCROLLABLE); lv_obj_add_flag(s_sb_track, LV_OBJ_FLAG_HIDDEN); s_sb_thumb = lv_obj_create(s_sb_track); lv_obj_set_size(s_sb_thumb, UI_SB_W, 40); lv_obj_set_pos(s_sb_thumb, 0, 0); lv_obj_set_style_bg_color(s_sb_thumb, COL_ACCENT, 0); lv_obj_set_style_bg_opa(s_sb_thumb, LV_OPA_COVER, 0); lv_obj_set_style_border_width(s_sb_thumb, 0, 0); lv_obj_set_style_radius(s_sb_thumb, 4, 0); lv_obj_set_style_pad_all(s_sb_thumb, 0, 0); lv_obj_clear_flag(s_sb_thumb, LV_OBJ_FLAG_SCROLLABLE); } } static void show_waiting(bool on) { if (on) { lv_obj_clear_flag(s_wait, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(s_main, LV_OBJ_FLAG_HIDDEN); } else { lv_obj_add_flag(s_wait, LV_OBJ_FLAG_HIDDEN); lv_obj_clear_flag(s_main, LV_OBJ_FLAG_HIDDEN); } s_waiting = on; } static void set_row_selected(ui_row_t *r, bool sel) { lv_obj_set_style_bg_color(r->row, sel ? COL_SEL : COL_BG, 0); lv_obj_set_style_text_color(r->name, sel ? COL_TEXT : COL_ACCENT2, 0); lv_obj_set_style_text_color(r->pct, sel ? COL_ACCENT : COL_MUTED, 0); lv_obj_set_style_bg_color(r->bar, sel ? COL_ACCENT : COL_BAR_HI, LV_PART_INDICATOR); } void ui_mixer_init(void) { lv_init(); s_disp = lv_display_create(LCD_WIDTH, LCD_HEIGHT); lv_display_set_flush_cb(s_disp, flush_cb); lv_display_set_buffers(s_disp, s_draw_buf, NULL, sizeof(s_draw_buf), LV_DISPLAY_RENDER_MODE_PARTIAL); lv_display_set_color_format(s_disp, LV_COLOR_FORMAT_RGB565); s_scr = lv_screen_active(); lv_obj_set_style_bg_color(s_scr, COL_BG, 0); lv_obj_set_style_bg_opa(s_scr, LV_OPA_COVER, 0); build_wait_screen(); build_main_screen(); show_waiting(true); s_last_tick = get_absolute_time(); s_master = 0xFF; s_master_peak = 0xFF; s_n = 0xFF; s_sel = 0xFF; s_page = 0xFF; s_count = 0xFF; memset(s_names, 0, sizeof(s_names)); memset(s_vols, 0, sizeof(s_vols)); memset(s_peaks, 0, sizeof(s_peaks)); /* First paint */ lv_timer_handler(); } void ui_mixer_task(void) { absolute_time_t now = get_absolute_time(); uint32_t elapsed = (uint32_t)absolute_time_diff_us(s_last_tick, now) / 1000u; if (elapsed > 0) { if (elapsed > 50) { elapsed = 50; } lv_tick_inc(elapsed); s_last_tick = now; } lv_timer_handler(); } bool ui_mixer_needs_tick(void) { return true; } void ui_mixer_draw(const mixer_state_t *st) { if (!st) { return; } if (!st->host_alive) { if (!s_waiting) { show_waiting(true); s_master = 0xFF; s_master_peak = 0xFF; s_n = 0xFF; s_sel = 0xFF; s_count = 0xFF; } return; } if (s_waiting) { show_waiting(false); s_master = 0xFF; s_master_peak = 0xFF; s_n = 0xFF; s_sel = 0xFF; s_page = 0xFF; s_count = 0xFF; } uint8_t n = st->n_visible; if (n > UI_MAX_ROWS) { n = UI_MAX_ROWS; } if (st->master_vol != s_master) { char buf[8]; snprintf(buf, sizeof(buf), "%u%%", st->master_vol); lv_label_set_text(s_master_pct, buf); lv_bar_set_value(s_master_bar, st->master_vol, LV_ANIM_ON); s_master = st->master_vol; } if (st->master_peak != s_master_peak) { set_vu_value(s_master_vu, st->master_peak); s_master_peak = st->master_peak; } bool list_changed = (n != s_n) || (st->selected != s_sel) || (st->page_start != s_page) || (st->session_count != s_count); if (!list_changed) { for (uint8_t i = 0; i < n; i++) { if (s_vols[i] != st->visible[i].vol || strncmp(s_names[i], st->visible[i].name, AUSIO_NAME_CHARS) != 0) { list_changed = true; break; } } } bool peaks_changed = false; if (!list_changed && s_n != 0xFF) { for (uint8_t i = 0; i < n; i++) { if (s_peaks[i] != st->visible[i].peak) { peaks_changed = true; break; } } if (!peaks_changed) { return; } for (uint8_t i = 0; i < n; i++) { if (s_peaks[i] != st->visible[i].peak) { set_vu_value(s_rows[i].vu, st->visible[i].peak); s_peaks[i] = st->visible[i].peak; } } return; } if (n == 0) { for (uint8_t i = 0; i < UI_MAX_ROWS; i++) { lv_obj_add_flag(s_rows[i].row, LV_OBJ_FLAG_HIDDEN); } lv_obj_add_flag(s_caret, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(s_sb_track, LV_OBJ_FLAG_HIDDEN); lv_obj_clear_flag(s_empty, LV_OBJ_FLAG_HIDDEN); set_vu_value(s_master_vu, 0); s_master_peak = 0; } else { lv_obj_add_flag(s_empty, LV_OBJ_FLAG_HIDDEN); lv_obj_clear_flag(s_caret, LV_OBJ_FLAG_HIDDEN); int rel = (int)st->selected - (int)st->page_start; if (rel < 0) { rel = 0; } if (rel >= (int)n) { rel = (int)n - 1; } bool sel_moved = (st->selected != s_sel) || (st->page_start != s_page) || (s_n == 0xFF); bool first = (s_n == 0xFF); for (uint8_t i = 0; i < UI_MAX_ROWS; i++) { ui_row_t *r = &s_rows[i]; if (i >= n) { lv_obj_add_flag(r->row, LV_OBJ_FLAG_HIDDEN); continue; } lv_obj_clear_flag(r->row, LV_OBJ_FLAG_HIDDEN); char name[AUSIO_NAME_CHARS + 1]; memcpy(name, st->visible[i].name, AUSIO_NAME_CHARS); name[AUSIO_NAME_CHARS] = '\0'; if (strncmp(s_names[i], name, AUSIO_NAME_CHARS + 1) != 0 || s_n == 0xFF) { lv_label_set_text(r->name, name); memcpy(s_names[i], name, sizeof(name)); } if (s_vols[i] != st->visible[i].vol || s_n == 0xFF) { char pct[8]; snprintf(pct, sizeof(pct), "%u%%", st->visible[i].vol); lv_label_set_text(r->pct, pct); lv_bar_set_value(r->bar, st->visible[i].vol, first ? LV_ANIM_OFF : LV_ANIM_ON); s_vols[i] = st->visible[i].vol; } set_vu_value(r->vu, st->visible[i].peak); s_peaks[i] = st->visible[i].peak; bool sel = ((int)i == rel); set_row_selected(r, sel); } move_caret_to((uint8_t)rel, !first && sel_moved); update_scrollbar(st->session_count, st->page_start, n); } s_n = n; s_sel = st->selected; s_page = st->page_start; s_count = st->session_count; }