Files
t-embed-insecam-widget/main/ui.c
T
Juli 3bdfcba6fb Selectable UI themes and a Popular source
Themes (Phosphor, Pink, Amber, Ice, Terminal) restyle the menu, boot
page and toasts live from the menu; all but Terminal use readable
Montserrat text. Source gains Popular: the top-rated listings walked in
rank order from page 1, alternating providers when mixed.
2026-09-17 03:11:25 +02:00

362 lines
13 KiB
C

#include "ui.h"
#include "display.h"
#include "esp_lvgl_port.h"
#include "lvgl.h"
#include <string.h>
// Themes for the "system" pages (menu, status, toast). The camera page keeps its own look.
typedef struct {
const char *name;
uint32_t bg, text, dim, sel_bg, sel_text, edit;
const lv_font_t *title_font, *row_font, *hint_font;
} theme_t;
static const theme_t themes[] = {
{"Phosphor", 0x000000, 0x3ddc84, 0x1f7a4a, 0x3ddc84, 0x04140b, 0xffb000,
&lv_font_montserrat_18, &lv_font_montserrat_14, &lv_font_montserrat_12},
{"Pink", 0x140812, 0xff6ab8, 0x8c3a6c, 0xff6ab8, 0x1e0716, 0xfff06a,
&lv_font_montserrat_18, &lv_font_montserrat_14, &lv_font_montserrat_12},
{"Amber", 0x000000, 0xffb000, 0x7a5200, 0xffb000, 0x1a1000, 0x3ddc84,
&lv_font_montserrat_18, &lv_font_montserrat_14, &lv_font_montserrat_12},
{"Ice", 0x03070d, 0xa6dcff, 0x3a6e91, 0x2fb8ff, 0x03070d, 0xffb000,
&lv_font_montserrat_18, &lv_font_montserrat_14, &lv_font_montserrat_12},
{"Terminal", 0x000000, 0x3ddc84, 0x1f7a4a, 0x3ddc84, 0x04140b, 0xffb000,
&lv_font_unscii_16, &lv_font_unscii_16, &lv_font_unscii_8},
};
static const theme_t *th = &themes[0];
static lv_obj_t *scr_status, *status_hdr, *lbl_title, *lbl_msg, *status_cursor;
static lv_obj_t *scr_cam, *canvas, *overlay, *lbl_place, *lbl_meta, *lbl_batt, *badge, *marker, *bar, *toast;
static lv_obj_t *scr_menu, *menu_title, *menu_cursor, *menu_rule, *menu_hint, *menu_row[UI_MENU_ROWS], *menu_label[UI_MENU_ROWS], *menu_value[UI_MENU_ROWS];
static lv_obj_t *prev_screen;
static lv_timer_t *cursor_timer;
static char menu_line[UI_MENU_ROWS][64];
static void cursor_blink_cb(lv_timer_t *t)
{
static bool on;
on = !on;
lv_obj_set_hidden(menu_cursor, !on);
lv_obj_set_hidden(status_cursor, !on);
}
static lv_timer_t *toast_timer;
static bool overlay_on = true;
static lv_obj_t *label(lv_obj_t *parent, const lv_font_t *font, uint32_t rgb)
{
lv_obj_t *l = lv_label_create(parent);
lv_obj_set_style_text_font(l, font, 0);
lv_obj_set_style_text_color(l, lv_color_hex(rgb), 0);
return l;
}
static lv_obj_t *plain_screen(void)
{
lv_obj_t *s = lv_obj_create(NULL);
lv_obj_remove_style_all(s);
lv_obj_set_size(s, LCD_H_RES, LCD_V_RES);
lv_obj_set_style_bg_color(s, lv_color_hex(0x000000), 0);
lv_obj_set_style_bg_opa(s, LV_OPA_COVER, 0);
return s;
}
static void toast_hide_cb(lv_timer_t *t)
{
lv_obj_set_hidden(toast, true);
toast_timer = NULL;
}
void ui_init(void)
{
lvgl_port_lock(0);
// --- status page ---
scr_status = plain_screen();
status_hdr = label(scr_status, th->hint_font, th->dim);
lv_label_set_text(status_hdr, "T-EMBED // ESP32-S3 // insecam widget");
lv_obj_set_pos(status_hdr, 12, 10);
lbl_title = label(scr_status, th->title_font, th->text);
lv_obj_set_width(lbl_title, LCD_H_RES - 24);
lv_obj_set_pos(lbl_title, 12, 30);
lbl_msg = label(scr_status, th->row_font, th->text);
lv_obj_set_width(lbl_msg, LCD_H_RES - 24);
lv_label_set_long_mode(lbl_msg, LV_LABEL_LONG_WRAP);
lv_obj_set_pos(lbl_msg, 12, 62);
status_cursor = label(scr_status, th->row_font, th->text);
lv_label_set_text(status_cursor, "_");
lv_obj_align(status_cursor, LV_ALIGN_BOTTOM_LEFT, 12, -10);
// --- camera page ---
scr_cam = plain_screen();
canvas = lv_canvas_create(scr_cam);
lv_obj_set_pos(canvas, 0, 0);
lv_obj_set_size(canvas, LCD_H_RES, LCD_V_RES);
overlay = lv_obj_create(scr_cam);
lv_obj_remove_style_all(overlay);
lv_obj_set_size(overlay, LCD_H_RES, 44);
lv_obj_align(overlay, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_style_bg_color(overlay, lv_color_hex(0x000000), 0);
lv_obj_set_style_bg_opa(overlay, LV_OPA_60, 0);
lbl_place = label(overlay, &lv_font_montserrat_18, 0xffffff);
lv_obj_set_width(lbl_place, LCD_H_RES - 16);
lv_label_set_long_mode(lbl_place, LV_LABEL_LONG_DOT);
lv_obj_set_pos(lbl_place, 8, 3);
lbl_meta = label(overlay, &lv_font_montserrat_12, 0xc4ccd8);
lv_obj_set_width(lbl_meta, LCD_H_RES - 76);
lv_label_set_long_mode(lbl_meta, LV_LABEL_LONG_DOT);
lv_obj_set_pos(lbl_meta, 8, 26);
lbl_batt = label(overlay, &lv_font_montserrat_12, 0xc4ccd8);
lv_obj_align(lbl_batt, LV_ALIGN_BOTTOM_RIGHT, -8, -3);
lv_obj_set_hidden(lbl_batt, true);
bar = lv_bar_create(scr_cam);
lv_obj_remove_style_all(bar);
lv_obj_set_size(bar, LCD_H_RES, 3);
lv_obj_align(bar, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_bar_set_range(bar, 0, 1000);
lv_obj_set_style_bg_color(bar, lv_color_hex(0xffffff), LV_PART_INDICATOR);
lv_obj_set_style_bg_opa(bar, LV_OPA_80, LV_PART_INDICATOR);
badge = label(scr_cam, &lv_font_montserrat_12, 0xffffff);
lv_obj_set_style_bg_opa(badge, LV_OPA_COVER, 0);
lv_obj_set_style_radius(badge, 3, 0);
lv_obj_set_style_pad_hor(badge, 5, 0);
lv_obj_set_style_pad_ver(badge, 2, 0);
lv_obj_align(badge, LV_ALIGN_TOP_RIGHT, -6, 6);
lv_obj_set_hidden(badge, true);
marker = label(scr_cam, &lv_font_montserrat_12, 0x101010);
lv_obj_set_style_bg_color(marker, lv_color_hex(0xffd040), 0);
lv_obj_set_style_bg_opa(marker, LV_OPA_COVER, 0);
lv_obj_set_style_radius(marker, 3, 0);
lv_obj_set_style_pad_hor(marker, 5, 0);
lv_obj_set_style_pad_ver(marker, 2, 0);
lv_obj_align(marker, LV_ALIGN_TOP_LEFT, 6, 6);
lv_obj_set_hidden(marker, true);
// toast lives on the top layer so it shows over every page
toast = label(lv_layer_top(), th->row_font, th->text);
lv_obj_set_style_bg_color(toast, lv_color_hex(th->bg), 0);
lv_obj_set_style_bg_opa(toast, LV_OPA_90, 0);
lv_obj_set_style_border_color(toast, lv_color_hex(th->text), 0);
lv_obj_set_style_border_width(toast, 1, 0);
lv_obj_set_style_radius(toast, 0, 0);
lv_obj_set_style_pad_all(toast, 8, 0);
lv_obj_align(toast, LV_ALIGN_CENTER, 0, -10);
lv_obj_set_hidden(toast, true);
// --- menu page: green phosphor terminal ---
scr_menu = plain_screen();
menu_title = label(scr_menu, th->title_font, th->text);
lv_obj_set_pos(menu_title, 6, 2);
menu_cursor = label(scr_menu, th->title_font, th->text);
lv_label_set_text(menu_cursor, "_");
menu_rule = lv_obj_create(scr_menu);
lv_obj_remove_style_all(menu_rule);
lv_obj_set_size(menu_rule, LCD_H_RES - 12, 1);
lv_obj_set_pos(menu_rule, 6, 23);
lv_obj_set_style_bg_opa(menu_rule, LV_OPA_COVER, 0);
for (int i = 0; i < UI_MENU_ROWS; i++) {
menu_row[i] = lv_obj_create(scr_menu);
lv_obj_remove_style_all(menu_row[i]);
lv_obj_set_size(menu_row[i], LCD_H_RES - 12, 20);
lv_obj_set_pos(menu_row[i], 6, 26 + i * 21);
lv_obj_set_style_bg_opa(menu_row[i], LV_OPA_COVER, 0);
menu_label[i] = label(menu_row[i], th->row_font, th->text);
lv_obj_set_width(menu_label[i], 200);
lv_label_set_long_mode(menu_label[i], LV_LABEL_LONG_CLIP);
lv_obj_align(menu_label[i], LV_ALIGN_LEFT_MID, 4, 0);
menu_value[i] = label(menu_row[i], th->row_font, th->text);
lv_obj_align(menu_value[i], LV_ALIGN_RIGHT_MID, -4, 0);
}
menu_hint = label(scr_menu, th->hint_font, th->dim);
lv_obj_set_pos(menu_hint, 6, 154);
cursor_timer = lv_timer_create(cursor_blink_cb, 500, NULL);
ui_set_theme(0);
lv_screen_load(scr_status);
lvgl_port_unlock();
}
void ui_status(const char *title, const char *msg)
{
lvgl_port_lock(0);
char buf[200];
snprintf(buf, sizeof buf, "> %s", title ? title : "");
lv_label_set_text(lbl_title, buf);
lv_label_set_text(lbl_msg, msg ? msg : "");
if (lv_screen_active() != scr_status) lv_screen_load(scr_status);
lvgl_port_unlock();
}
void ui_show_camera_page(void)
{
lvgl_port_lock(0);
if (lv_screen_active() != scr_cam) lv_screen_load(scr_cam);
lvgl_port_unlock();
}
void ui_present_frame(uint16_t *rgb565)
{
lvgl_port_lock(0);
lv_canvas_set_buffer(canvas, rgb565, LCD_H_RES, LCD_V_RES, LV_COLOR_FORMAT_RGB565);
lv_obj_invalidate(canvas);
lvgl_port_unlock();
}
void ui_set_caption(const char *place, const char *meta)
{
lvgl_port_lock(0);
lv_label_set_text(lbl_place, place ? place : "");
lv_label_set_text(lbl_meta, meta ? meta : "");
lvgl_port_unlock();
}
void ui_set_badge(const char *text, uint32_t rgb)
{
lvgl_port_lock(0);
if (!text || !overlay_on) {
lv_obj_set_hidden(badge, true);
} else {
lv_label_set_text(badge, text);
lv_obj_set_style_bg_color(badge, lv_color_hex(rgb), 0);
lv_obj_set_hidden(badge, false);
}
lvgl_port_unlock();
}
void ui_set_progress(int permille)
{
lvgl_port_lock(0);
if (permille < 0 || !overlay_on) {
lv_obj_set_hidden(bar, true);
} else {
lv_bar_set_value(bar, permille > 1000 ? 1000 : permille, LV_ANIM_OFF);
lv_obj_set_hidden(bar, false);
}
lvgl_port_unlock();
}
void ui_toast(const char *msg, uint32_t ms)
{
lvgl_port_lock(0);
lv_label_set_text(toast, msg);
lv_obj_set_hidden(toast, false);
if (toast_timer) lv_timer_delete(toast_timer);
toast_timer = lv_timer_create(toast_hide_cb, ms, NULL);
lv_timer_set_repeat_count(toast_timer, 1);
lvgl_port_unlock();
}
void ui_set_overlay_visible(bool visible)
{
lvgl_port_lock(0);
overlay_on = visible;
if (visible) {
lv_obj_set_hidden(overlay, false);
} else {
lv_obj_set_hidden(overlay, true);
lv_obj_set_hidden(badge, true);
lv_obj_set_hidden(marker, true);
lv_obj_set_hidden(bar, true);
}
lvgl_port_unlock();
}
bool ui_overlay_visible(void) { return overlay_on; }
void ui_set_marker(const char *text)
{
lvgl_port_lock(0);
if (!text || !overlay_on) {
lv_obj_set_hidden(marker, true);
} else {
lv_label_set_text(marker, text);
lv_obj_set_hidden(marker, false);
}
lvgl_port_unlock();
}
void ui_menu_show(const char *title, const ui_menu_row_t *rows, int count, int top, int selected, bool editing)
{
lvgl_port_lock(0);
char buf[64];
snprintf(buf, sizeof buf, "T-EMBED://%s", title);
for (char *p = buf; *p; p++) *p = (*p >= 'a' && *p <= 'z') ? *p - 32 : *p;
lv_label_set_text(menu_title, buf);
lv_obj_update_layout(menu_title);
lv_obj_set_pos(menu_cursor, 6 + lv_obj_get_width(menu_title), 2);
for (int i = 0; i < UI_MENU_ROWS; i++) {
int idx = top + i;
bool present = idx < count;
lv_obj_set_hidden(menu_row[i], !present);
if (!present) continue;
bool sel = idx == selected;
uint32_t fg = sel ? th->sel_text : rows[idx].dim ? th->dim : th->text;
lv_obj_set_style_bg_color(menu_row[i], lv_color_hex(sel ? th->sel_bg : th->bg), 0);
snprintf(menu_line[i], sizeof menu_line[i], "%02d%c%s", idx + 1, sel ? '>' : ' ', rows[idx].label);
lv_label_set_text(menu_label[i], menu_line[i]);
lv_obj_set_style_text_color(menu_label[i], lv_color_hex(fg), 0);
lv_label_set_text(menu_value[i], rows[idx].value ? rows[idx].value : "");
lv_obj_set_style_text_color(menu_value[i], lv_color_hex(sel && editing ? th->edit : fg), 0);
}
snprintf(buf, sizeof buf, "[%02d/%02d] turn:move click:%s hold:back", selected + 1, count, editing ? "set" : "ok");
lv_label_set_text(menu_hint, buf);
if (lv_screen_active() != scr_menu) {
prev_screen = lv_screen_active();
lv_screen_load(scr_menu);
}
lvgl_port_unlock();
}
void ui_menu_hide(void)
{
lvgl_port_lock(0);
if (lv_screen_active() == scr_menu) lv_screen_load(prev_screen ? prev_screen : scr_cam);
lvgl_port_unlock();
}
void ui_set_battery(const char *text)
{
lvgl_port_lock(0);
if (!text) lv_obj_set_hidden(lbl_batt, true);
else { lv_label_set_text(lbl_batt, text); lv_obj_set_hidden(lbl_batt, false); }
lvgl_port_unlock();
}
int ui_theme_count(void) { return sizeof themes / sizeof themes[0]; }
const char *ui_theme_name(int idx) { return themes[idx < 0 || idx >= ui_theme_count() ? 0 : idx].name; }
static void apply(lv_obj_t *l, const lv_font_t *f, uint32_t rgb)
{
lv_obj_set_style_text_font(l, f, 0);
lv_obj_set_style_text_color(l, lv_color_hex(rgb), 0);
}
void ui_set_theme(int idx)
{
if (idx < 0 || idx >= ui_theme_count()) idx = 0;
lvgl_port_lock(0);
th = &themes[idx];
lv_obj_set_style_bg_color(scr_status, lv_color_hex(th->bg), 0);
lv_obj_set_style_bg_color(scr_menu, lv_color_hex(th->bg), 0);
apply(status_hdr, th->hint_font, th->dim);
apply(lbl_title, th->title_font, th->text);
apply(lbl_msg, th->row_font, th->text);
apply(status_cursor, th->row_font, th->text);
apply(menu_title, th->title_font, th->text);
apply(menu_cursor, th->title_font, th->text);
lv_obj_set_style_bg_color(menu_rule, lv_color_hex(th->dim), 0);
for (int i = 0; i < UI_MENU_ROWS; i++) {
lv_obj_set_style_text_font(menu_label[i], th->row_font, 0);
lv_obj_set_style_text_font(menu_value[i], th->row_font, 0);
}
apply(menu_hint, th->hint_font, th->dim);
apply(toast, th->row_font, th->text);
lv_obj_set_style_bg_color(toast, lv_color_hex(th->bg), 0);
lv_obj_set_style_border_color(toast, lv_color_hex(th->text), 0);
lvgl_port_unlock();
}