diff --git a/CMakeLists.txt b/CMakeLists.txt index 91d10e8..4b06583 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,7 @@ project(new_kbd) zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/inc) zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/events) +zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/ui) zephyr_compile_definitions( LV_LVGL_H_INCLUDE_SIMPLE=1 @@ -56,6 +57,7 @@ target_sources(app PRIVATE src/modules/time_manager_module.c src/modules/usb_hid_module.c src/modules/ble_hid_module.c + src/ui/display_ui.c src/ui/fonts/ui_font_keyboard_small_18.c src/ui/fonts/ui_font_keyboard_time_48.c ) diff --git a/src/modules/display_module.c b/src/modules/display_module.c index 4005ec3..f85684b 100644 --- a/src/modules/display_module.c +++ b/src/modules/display_module.c @@ -1,6 +1,6 @@ #include -#include #include +#include #include #include @@ -21,6 +21,7 @@ #include "battery_status_event.h" #include "display_theme_event.h" +#include "display_ui.h" #include "keyboard_led_event.h" #include "mode_event.h" #include "time_manager.h" @@ -39,19 +40,6 @@ LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF); #define DISPLAY_DEMO_BASE_HOUR 14 #define DISPLAY_DEMO_BASE_MIN 28 #define DISPLAY_DEMO_BASE_SEC 36 -#define DISPLAY_SYMBOL_PLUG "\xEF\x87\xA6" /* U+F1E6, custom plug glyph in ui_font_keyboard_small_18 */ - -LV_FONT_DECLARE(ui_font_keyboard_small_18); -LV_FONT_DECLARE(ui_font_keyboard_time_48); - -enum display_status_id -{ - DISPLAY_STATUS_USB = 0, - DISPLAY_STATUS_BLE, - DISPLAY_STATUS_NUMLOCK, - DISPLAY_STATUS_CAPSLOCK, - DISPLAY_STATUS_COUNT, -}; enum display_pm_state { @@ -59,39 +47,6 @@ enum display_pm_state DISPLAY_PM_STATE_OFF, }; -struct display_ui_state -{ - lv_color_t theme_color; - lv_color_t inactive_border_color; - uint8_t battery_level; - mode_type_t mode; - uint8_t led_mask; - uint8_t battery_flags; - bool status_enabled[DISPLAY_STATUS_COUNT]; - lv_obj_t *status_badges[DISPLAY_STATUS_COUNT]; - lv_obj_t *status_labels[DISPLAY_STATUS_COUNT]; - lv_obj_t *battery_icon; - lv_obj_t *battery_label; - lv_obj_t *battery_state_label; - lv_obj_t *date_label; - lv_obj_t *time_label; -}; - -struct display_ctx -{ - const struct device *dev; - struct display_capabilities caps; - struct k_work_delayable update_work; - struct k_work_delayable idle_work; - struct k_work_delayable theme_save_work; - struct display_ui_state ui; - uint32_t tick_count; - enum display_pm_state pm_state; - bool initialized; - bool theme_storage_dirty; - bool theme_storage_loaded; -}; - struct display_theme_storage { uint8_t red; uint8_t green; @@ -99,6 +54,23 @@ struct display_theme_storage { uint8_t valid_marker; }; +struct display_ctx +{ + const struct device *dev; + struct display_capabilities caps; + struct k_work_delayable update_work; + struct k_work_delayable idle_work; + struct k_work_delayable theme_save_work; + struct display_ui_model ui; + uint32_t tick_count; + enum display_pm_state pm_state; + bool initialized; + bool theme_storage_dirty; + bool theme_storage_loaded; + char date_text[16]; + char time_text[16]; +}; + static struct display_ctx disp = { .dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display)), .ui.theme_color = LV_COLOR_MAKE(0x4C, 0xC9, 0xF0), @@ -106,7 +78,7 @@ static struct display_ctx disp = { .ui.battery_level = 15U, .ui.battery_flags = 0U, .ui.mode = MODE_TYPE_USB, - .ui.status_enabled = {true, true, false, true}, + .ui.led_mask = 0U, .pm_state = DISPLAY_PM_STATE_OFF, }; @@ -115,15 +87,6 @@ static struct display_theme_storage display_theme_storage; static const struct led_dt_spec display_backlight = LED_DT_SPEC_GET(DT_NODELABEL(backlight)); -static const char *const g_status_texts[DISPLAY_STATUS_COUNT] = { - LV_SYMBOL_USB, - LV_SYMBOL_BLUETOOTH, - "1", - "A", -}; - -static void display_refresh_all_locked(void); - static int display_theme_store(const struct display_theme_storage *storage) { char key[] = MODULE_NAME "/" DISPLAY_THEME_STORAGE_KEY; @@ -140,9 +103,9 @@ static int display_theme_store(const struct display_theme_storage *storage) } static void display_theme_set_rgb(uint8_t red, - uint8_t green, - uint8_t blue, - bool persist) + uint8_t green, + uint8_t blue, + bool persist) { disp.ui.theme_color = lv_color_make(red, green, blue); @@ -226,7 +189,6 @@ static void display_schedule_idle_timeout(k_timeout_t delay) k_work_reschedule(&disp.idle_work, delay); } -/* 背光初始化独立处理,避免 UI 创建逻辑里混入硬件使能细节。 */ static int display_backlight_set(uint8_t brightness) { int err; @@ -252,7 +214,59 @@ static bool display_is_active(void) return disp.pm_state == DISPLAY_PM_STATE_ACTIVE; } -/* 只负责保活屏幕空闲计时,不隐式点亮屏幕。 */ +static void display_update_datetime_text(void) +{ + struct time_manager_snapshot snapshot; + int err = time_manager_get_snapshot(&snapshot); + + if (!err) + { + time_t local_seconds; + struct tm tm_buf; + struct tm *tm_info; + + local_seconds = (time_t)(snapshot.utc_ms / 1000ULL) + + (time_t)((int32_t)snapshot.timezone_min * 60); + tm_info = gmtime_r(&local_seconds, &tm_buf); + + if (tm_info) + { + unsigned int year = (unsigned int)(tm_info->tm_year + 1900); + unsigned int month = (unsigned int)(tm_info->tm_mon + 1); + unsigned int day = (unsigned int)tm_info->tm_mday; + unsigned int hour = (unsigned int)tm_info->tm_hour; + unsigned int minute = (unsigned int)tm_info->tm_min; + unsigned int second = (unsigned int)tm_info->tm_sec; + + snprintk(disp.date_text, sizeof(disp.date_text), "%04u/%02u/%02u", + year, month, day); + snprintk(disp.time_text, sizeof(disp.time_text), "%02u:%02u:%02u", + hour, minute, second); + return; + } + } + + { + uint32_t seconds = disp.tick_count; + uint32_t hour = (DISPLAY_DEMO_BASE_HOUR + (seconds / 3600U)) % 24U; + uint32_t minute = (DISPLAY_DEMO_BASE_MIN + ((seconds / 60U) % 60U)) % 60U; + uint32_t second = (DISPLAY_DEMO_BASE_SEC + (seconds % 60U)) % 60U; + + snprintk(disp.date_text, sizeof(disp.date_text), "%04d/%02d/%02d", + DISPLAY_DEMO_BASE_YEAR, + DISPLAY_DEMO_BASE_MONTH, + DISPLAY_DEMO_BASE_DAY); + snprintk(disp.time_text, sizeof(disp.time_text), "%02u:%02u:%02u", + hour, minute, second); + } +} + +static void display_refresh_all_locked(void) +{ + display_update_datetime_text(); + display_ui_refresh_all(&disp.ui, disp.date_text, disp.time_text); +} + static void display_kick_idle_timer(void) { if (!disp.initialized || !display_is_active()) @@ -261,7 +275,6 @@ static void display_kick_idle_timer(void) display_schedule_idle_timeout(K_MINUTES(DISPLAY_IDLE_TIMEOUT_MIN)); } -/* 熄屏时同时关闭刷新和背光,并将模块状态切到 OFF。 */ static void display_sleep(void) { int err; @@ -287,7 +300,6 @@ static void display_sleep(void) module_set_state(MODULE_STATE_OFF); } -/* 唤醒屏幕后立刻刷新 UI,并重新启动定时刷新和空闲超时。 */ static void display_wake(void) { int err; @@ -323,309 +335,6 @@ static void display_idle_timeout_fn(struct k_work *work) display_sleep(); } -/* 电量颜色与 PC 原型保持一致,顶部状态区能快速表达健康度。 */ -static lv_color_t display_get_battery_color(uint8_t battery_level) -{ - if (battery_level > 70U) - return lv_color_hex(0x8BD450); - - if (battery_level >= 20U) - return lv_color_hex(0xF4D35E); - - return lv_color_hex(0xE63946); -} - -/* 电池图标由精简图标字体提供,不再依赖 LVGL 内建字体资源。 */ -static const char *display_get_battery_symbol(uint8_t battery_level) -{ - if (battery_level > 85U) - return LV_SYMBOL_BATTERY_FULL; - - if (battery_level > 60U) - return LV_SYMBOL_BATTERY_3; - - if (battery_level > 35U) - return LV_SYMBOL_BATTERY_2; - - if (battery_level >= 20U) - return LV_SYMBOL_BATTERY_1; - - return LV_SYMBOL_BATTERY_EMPTY; -} - -/* 模式事件只需要驱动 USB/BLE 两个 badge,2.4G 模式两者都灭。 */ -static void display_update_mode_state(mode_type_t mode) -{ - disp.ui.mode = mode; - disp.ui.status_enabled[DISPLAY_STATUS_USB] = (mode == MODE_TYPE_USB); - disp.ui.status_enabled[DISPLAY_STATUS_BLE] = (mode == MODE_TYPE_BLE); -} - -/* 最新原型只显示 NumLock 和 CapsLock,不再展示 ScrollLock。 */ -static void display_update_keyboard_led_state(uint8_t led_mask) -{ - disp.ui.led_mask = led_mask; - disp.ui.status_enabled[DISPLAY_STATUS_NUMLOCK] = - (led_mask & KEYBOARD_LED_MASK_NUM_LOCK) != 0U; - disp.ui.status_enabled[DISPLAY_STATUS_CAPSLOCK] = - (led_mask & KEYBOARD_LED_MASK_CAPS_LOCK) != 0U; -} - -/* 底部状态条的亮灭与边框颜色联动更新,保持原型机视觉语言。 */ -static void display_refresh_status_bar_locked(void) -{ - for (uint32_t i = 0; i < DISPLAY_STATUS_COUNT; i++) - { - lv_obj_t *badge = disp.ui.status_badges[i]; - lv_obj_t *label = disp.ui.status_labels[i]; - bool active = disp.ui.status_enabled[i]; - - if (!badge || !label) - continue; - - lv_obj_set_style_border_width(badge, 4, 0); - lv_obj_set_style_border_color(badge, - active ? disp.ui.theme_color : disp.ui.inactive_border_color, - 0); - lv_obj_set_style_bg_color(badge, - active ? lv_color_hex(0x1D2735) : lv_color_hex(0x161A20), - 0); - lv_obj_set_style_text_color(label, - active ? lv_color_white() : lv_color_hex(0x7C8798), - 0); - } -} - -/* 电池图标、百分比和状态图标分开更新,便于独立配色。 */ -static void display_refresh_battery_locked(void) -{ - char battery_text[8]; - lv_color_t battery_color; - const char *state_symbol = ""; - lv_color_t state_color = lv_color_white(); - - if (!disp.ui.battery_icon || !disp.ui.battery_label || !disp.ui.battery_state_label) - return; - - battery_color = display_get_battery_color(disp.ui.battery_level); - snprintk(battery_text, sizeof(battery_text), "%u%%", disp.ui.battery_level); - - if ((disp.ui.battery_flags & BATTERY_STATUS_FLAG_FULL) != 0U) - { - state_symbol = DISPLAY_SYMBOL_PLUG; - state_color = lv_color_hex(0x4C9EF5); - } - else if ((disp.ui.battery_flags & BATTERY_STATUS_FLAG_CHARGING) != 0U) - { - state_symbol = LV_SYMBOL_CHARGE; - state_color = lv_color_hex(0xF4D35E); - } - - lv_label_set_text(disp.ui.battery_icon, - display_get_battery_symbol(disp.ui.battery_level)); - lv_obj_set_style_text_color(disp.ui.battery_icon, battery_color, 0); - lv_label_set_text(disp.ui.battery_label, battery_text); - lv_label_set_text(disp.ui.battery_state_label, state_symbol); - lv_obj_set_style_text_color(disp.ui.battery_state_label, state_color, 0); -} - -/* - * 时间优先显示 time_manager 的真实快照。 - * 如果当前尚未同步,则退回到固定基准上的 demo 时间,保证 UI 结构始终可见。 - */ -static void display_refresh_datetime_locked(void) -{ - struct time_manager_snapshot snapshot; - char date_text[16]; - char time_text[16]; - int err = time_manager_get_snapshot(&snapshot); - - if (!disp.ui.date_label || !disp.ui.time_label) - return; - - if (!err) - { - time_t local_seconds; - struct tm tm_buf; - struct tm *tm_info; - - local_seconds = (time_t)(snapshot.utc_ms / 1000ULL) + - (time_t)((int32_t)snapshot.timezone_min * 60); - tm_info = gmtime_r(&local_seconds, &tm_buf); - - if (tm_info) - { - unsigned int year = (unsigned int)(tm_info->tm_year + 1900); - unsigned int month = (unsigned int)(tm_info->tm_mon + 1); - unsigned int day = (unsigned int)tm_info->tm_mday; - unsigned int hour = (unsigned int)tm_info->tm_hour; - unsigned int minute = (unsigned int)tm_info->tm_min; - unsigned int second = (unsigned int)tm_info->tm_sec; - - snprintk(date_text, sizeof(date_text), "%04u/%02u/%02u", - year, month, day); - snprintk(time_text, sizeof(time_text), "%02u:%02u:%02u", - hour, minute, second); - lv_label_set_text(disp.ui.date_label, date_text); - lv_label_set_text(disp.ui.time_label, time_text); - return; - } - } - - { - uint32_t seconds = disp.tick_count; - uint32_t hour = (DISPLAY_DEMO_BASE_HOUR + (seconds / 3600U)) % 24U; - uint32_t minute = (DISPLAY_DEMO_BASE_MIN + ((seconds / 60U) % 60U)) % 60U; - uint32_t second = (DISPLAY_DEMO_BASE_SEC + (seconds % 60U)) % 60U; - - snprintk(date_text, sizeof(date_text), "%04d/%02d/%02d", - DISPLAY_DEMO_BASE_YEAR, - DISPLAY_DEMO_BASE_MONTH, - DISPLAY_DEMO_BASE_DAY); - snprintk(time_text, sizeof(time_text), "%02u:%02u:%02u", - hour, minute, second); - lv_label_set_text(disp.ui.date_label, date_text); - lv_label_set_text(disp.ui.time_label, time_text); - } -} - -/* 一次性把缓存状态刷到 UI,避免控件创建与状态恢复互相耦合。 */ -static void display_refresh_all_locked(void) -{ - display_refresh_status_bar_locked(); - display_refresh_battery_locked(); - display_refresh_datetime_locked(); -} - -/* 状态 badge 保持原型尺寸和圆角,确保在 320x172 面板上视觉一致。 */ -static void display_create_status_chip_locked(lv_obj_t *parent, enum display_status_id id) -{ - lv_obj_t *badge = lv_obj_create(parent); - lv_obj_t *label; - - lv_obj_remove_style_all(badge); - lv_obj_set_size(badge, 50, 32); - lv_obj_set_style_radius(badge, 10, 0); - lv_obj_set_style_bg_opa(badge, LV_OPA_COVER, 0); - lv_obj_set_style_pad_all(badge, 0, 0); - - label = lv_label_create(badge); - lv_label_set_text(label, g_status_texts[id]); - lv_obj_set_width(label, LV_PCT(100)); - lv_obj_set_style_text_font(label, &ui_font_keyboard_small_18, 0); - lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0); - lv_obj_center(label); - - disp.ui.status_badges[id] = badge; - disp.ui.status_labels[id] = label; -} - -/* UI 直接内联到 display_module,保留原型布局而不引入额外 ui 抽象层。 */ -static void display_create_ui_locked(void) -{ - lv_obj_t *screen = lv_screen_active(); - lv_obj_t *content; - lv_obj_t *top_row; - lv_obj_t *battery_wrap; - lv_obj_t *middle_row; - lv_obj_t *bottom_row; - - lv_obj_clean(screen); - lv_obj_set_style_bg_color(screen, lv_color_hex(0x0F1115), 0); - lv_obj_set_style_bg_grad_color(screen, lv_color_hex(0x1A1F29), 0); - lv_obj_set_style_bg_grad_dir(screen, LV_GRAD_DIR_VER, 0); - lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0); - lv_obj_set_style_text_color(screen, lv_color_white(), 0); - lv_obj_set_style_pad_all(screen, 0, 0); - lv_obj_set_scrollbar_mode(screen, LV_SCROLLBAR_MODE_OFF); - - content = lv_obj_create(screen); - lv_obj_remove_style_all(content); - lv_obj_set_size(content, LV_PCT(100), LV_PCT(100)); - lv_obj_set_style_bg_color(content, lv_color_hex(0x0F1115), 0); - lv_obj_set_style_bg_opa(content, LV_OPA_TRANSP, 0); - lv_obj_set_style_pad_left(content, 14, 0); - lv_obj_set_style_pad_right(content, 14, 0); - lv_obj_set_style_pad_top(content, 8, 0); - lv_obj_set_style_pad_bottom(content, 8, 0); - lv_obj_set_layout(content, LV_LAYOUT_FLEX); - lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN); - lv_obj_set_flex_align(content, - LV_FLEX_ALIGN_START, - LV_FLEX_ALIGN_CENTER, - LV_FLEX_ALIGN_CENTER); - - top_row = lv_obj_create(content); - lv_obj_remove_style_all(top_row); - lv_obj_set_width(top_row, LV_PCT(100)); - lv_obj_set_flex_grow(top_row, 1); - lv_obj_set_style_bg_color(top_row, lv_color_hex(0x0F1115), 0); - lv_obj_set_style_bg_opa(top_row, LV_OPA_TRANSP, 0); - lv_obj_set_layout(top_row, LV_LAYOUT_FLEX); - lv_obj_set_flex_flow(top_row, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(top_row, - LV_FLEX_ALIGN_SPACE_BETWEEN, - LV_FLEX_ALIGN_CENTER, - LV_FLEX_ALIGN_CENTER); - - disp.ui.date_label = lv_label_create(top_row); - lv_obj_set_style_text_font(disp.ui.date_label, &ui_font_keyboard_small_18, 0); - lv_obj_set_style_text_color(disp.ui.date_label, lv_color_hex(0xD8DEE9), 0); - - battery_wrap = lv_obj_create(top_row); - lv_obj_remove_style_all(battery_wrap); - lv_obj_set_width(battery_wrap, LV_SIZE_CONTENT); - lv_obj_set_layout(battery_wrap, LV_LAYOUT_FLEX); - lv_obj_set_flex_flow(battery_wrap, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(battery_wrap, - LV_FLEX_ALIGN_CENTER, - LV_FLEX_ALIGN_CENTER, - LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_column(battery_wrap, 4, 0); - - disp.ui.battery_icon = lv_label_create(battery_wrap); - lv_obj_set_style_text_font(disp.ui.battery_icon, &ui_font_keyboard_small_18, 0); - - disp.ui.battery_label = lv_label_create(battery_wrap); - lv_obj_set_style_text_font(disp.ui.battery_label, &ui_font_keyboard_small_18, 0); - lv_obj_set_style_text_color(disp.ui.battery_label, lv_color_hex(0xD8DEE9), 0); - - disp.ui.battery_state_label = lv_label_create(battery_wrap); - lv_obj_set_style_text_font(disp.ui.battery_state_label, &ui_font_keyboard_small_18, 0); - - middle_row = lv_obj_create(content); - lv_obj_remove_style_all(middle_row); - lv_obj_set_width(middle_row, LV_PCT(100)); - lv_obj_set_flex_grow(middle_row, 2); - lv_obj_set_style_bg_color(middle_row, lv_color_hex(0x0F1115), 0); - lv_obj_set_style_bg_opa(middle_row, LV_OPA_TRANSP, 0); - - disp.ui.time_label = lv_label_create(middle_row); - lv_obj_set_style_text_font(disp.ui.time_label, &ui_font_keyboard_time_48, 0); - lv_obj_set_style_text_color(disp.ui.time_label, lv_color_white(), 0); - lv_obj_center(disp.ui.time_label); - - bottom_row = lv_obj_create(content); - lv_obj_remove_style_all(bottom_row); - lv_obj_set_width(bottom_row, LV_PCT(100)); - lv_obj_set_flex_grow(bottom_row, 1); - lv_obj_set_style_bg_color(bottom_row, lv_color_hex(0x0F1115), 0); - lv_obj_set_style_bg_opa(bottom_row, LV_OPA_TRANSP, 0); - lv_obj_set_layout(bottom_row, LV_LAYOUT_FLEX); - lv_obj_set_flex_flow(bottom_row, LV_FLEX_FLOW_ROW); - lv_obj_set_flex_align(bottom_row, - LV_FLEX_ALIGN_CENTER, - LV_FLEX_ALIGN_CENTER, - LV_FLEX_ALIGN_CENTER); - lv_obj_set_style_pad_column(bottom_row, 6, 0); - - for (uint32_t i = 0; i < DISPLAY_STATUS_COUNT; i++) - display_create_status_chip_locked(bottom_row, (enum display_status_id)i); - - display_refresh_all_locked(); -} - -/* 周期刷新只负责时间区域;状态图标改为事件驱动,避免无谓重绘。 */ static void display_update_work_fn(struct k_work *work) { ARG_UNUSED(work); @@ -637,15 +346,15 @@ static void display_update_work_fn(struct k_work *work) return; disp.tick_count++; + display_update_datetime_text(); lvgl_lock(); - display_refresh_datetime_locked(); + display_ui_refresh_datetime(disp.date_text, disp.time_text); lvgl_unlock(); display_schedule_update(K_MSEC(DISPLAY_UPDATE_PERIOD_MS)); } -/* 显示初始化完成后,后续 UI 更新全部通过事件和定时刷新驱动。 */ static int display_init(void) { int err; @@ -658,15 +367,16 @@ static int display_init(void) display_get_capabilities(disp.dev, &disp.caps); LOG_INF("Display caps: %ux%u fmt=%d", - disp.caps.x_resolution, - disp.caps.y_resolution, - disp.caps.current_pixel_format); + disp.caps.x_resolution, + disp.caps.y_resolution, + disp.caps.current_pixel_format); k_work_init_delayable(&disp.update_work, display_update_work_fn); k_work_init_delayable(&disp.idle_work, display_idle_timeout_fn); k_work_init_delayable(&disp.theme_save_work, display_theme_save_work_fn); disp.tick_count = 0U; display_theme_apply_loaded_storage(); + display_update_datetime_text(); err = display_blanking_off(disp.dev); if (err) @@ -680,7 +390,7 @@ static int display_init(void) return err; lvgl_lock(); - display_create_ui_locked(); + display_ui_init(&disp.ui, disp.date_text, disp.time_text); lvgl_unlock(); disp.initialized = true; @@ -692,54 +402,45 @@ static int display_init(void) return 0; } -/* 电池事件只缓存最新 SOC,UI 若已就绪则立即刷新顶部电池区域。 */ static bool handle_battery_status_event(const struct battery_status_event *event) { disp.ui.battery_level = battery_status_event_get_soc(event); disp.ui.battery_flags = battery_status_event_get_flags(event); - if (!disp.initialized) - return false; - - if (!display_is_active()) + if (!disp.initialized || !display_is_active()) { return false; + } lvgl_lock(); - display_refresh_battery_locked(); + display_ui_refresh_battery(&disp.ui); lvgl_unlock(); return false; } -/* 模式事件只影响 USB/BLE 两个 badge 的亮灭。 */ static bool handle_mode_event(const struct mode_event *event) { - display_update_mode_state(event->mode_type); + disp.ui.mode = event->mode_type; - if (!disp.initialized) - return false; - - if (!display_is_active()) + if (!disp.initialized || !display_is_active()) { return false; + } lvgl_lock(); - display_refresh_status_bar_locked(); + display_ui_refresh_status_bar(&disp.ui); lvgl_unlock(); return false; } -/* NumLock/CapsLock/ScrollLock 变化后,底部三个状态 badge 立即更新。 */ static bool handle_keyboard_led_event(const struct keyboard_led_event *event) { - display_update_keyboard_led_state(keyboard_led_event_get_mask(event)); + disp.ui.led_mask = keyboard_led_event_get_mask(event); - if (!disp.initialized) - return false; - - if (!display_is_active()) + if (!disp.initialized || !display_is_active()) { return false; + } lvgl_lock(); - display_refresh_status_bar_locked(); + display_ui_refresh_status_bar(&disp.ui); lvgl_unlock(); return false; } @@ -753,12 +454,11 @@ static bool handle_display_theme_event(const struct display_theme_event *event) } lvgl_lock(); - display_refresh_status_bar_locked(); + display_ui_refresh_status_bar(&disp.ui); lvgl_unlock(); return false; } -/* 任意按钮事件都可点亮屏幕并重置 1 分钟空闲计时。 */ static bool handle_button_event(const struct button_event *event) { ARG_UNUSED(event); @@ -786,7 +486,7 @@ static bool handle_module_state_event(const struct module_state_event *event) if (disp.initialized && display_is_active()) { lvgl_lock(); - display_refresh_status_bar_locked(); + display_ui_refresh_status_bar(&disp.ui); lvgl_unlock(); } diff --git a/src/ui/display_ui.c b/src/ui/display_ui.c new file mode 100644 index 0000000..b8573e7 --- /dev/null +++ b/src/ui/display_ui.c @@ -0,0 +1,306 @@ +#include + +#include +#include + +#include "battery_status_event.h" +#include "display_ui.h" +#include "keyboard_led_event.h" + +#define DISPLAY_SYMBOL_PLUG "\xEF\x87\xA6" + +LV_FONT_DECLARE(ui_font_keyboard_small_18); +LV_FONT_DECLARE(ui_font_keyboard_time_48); + +enum display_status_id +{ + DISPLAY_STATUS_USB = 0, + DISPLAY_STATUS_BLE, + DISPLAY_STATUS_NUMLOCK, + DISPLAY_STATUS_CAPSLOCK, + DISPLAY_STATUS_COUNT, +}; + +struct display_ui_ctx +{ + lv_obj_t *status_badges[DISPLAY_STATUS_COUNT]; + lv_obj_t *status_labels[DISPLAY_STATUS_COUNT]; + lv_obj_t *battery_icon; + lv_obj_t *battery_label; + lv_obj_t *battery_state_label; + lv_obj_t *date_label; + lv_obj_t *time_label; +}; + +static struct display_ui_ctx g_display_ui; + +static const char *const g_status_texts[DISPLAY_STATUS_COUNT] = { + LV_SYMBOL_USB, + LV_SYMBOL_BLUETOOTH, + "1", + "A", +}; + +static lv_color_t display_ui_get_battery_color(uint8_t battery_level) +{ + if (battery_level > 70U) { + return lv_color_hex(0x8BD450); + } + + if (battery_level >= 20U) { + return lv_color_hex(0xF4D35E); + } + + return lv_color_hex(0xE63946); +} + +static const char *display_ui_get_battery_symbol(uint8_t battery_level) +{ + if (battery_level > 85U) { + return LV_SYMBOL_BATTERY_FULL; + } + + if (battery_level > 60U) { + return LV_SYMBOL_BATTERY_3; + } + + if (battery_level > 35U) { + return LV_SYMBOL_BATTERY_2; + } + + if (battery_level >= 20U) { + return LV_SYMBOL_BATTERY_1; + } + + return LV_SYMBOL_BATTERY_EMPTY; +} + +static bool display_ui_status_is_active(enum display_status_id id, + const struct display_ui_model *model) +{ + switch (id) { + case DISPLAY_STATUS_USB: + return model->mode == MODE_TYPE_USB; + + case DISPLAY_STATUS_BLE: + return model->mode == MODE_TYPE_BLE; + + case DISPLAY_STATUS_NUMLOCK: + return (model->led_mask & KEYBOARD_LED_MASK_NUM_LOCK) != 0U; + + case DISPLAY_STATUS_CAPSLOCK: + return (model->led_mask & KEYBOARD_LED_MASK_CAPS_LOCK) != 0U; + + default: + return false; + } +} + +static void display_ui_create_status_chip(lv_obj_t *parent, enum display_status_id id) +{ + lv_obj_t *badge = lv_obj_create(parent); + lv_obj_t *label; + + lv_obj_remove_style_all(badge); + lv_obj_set_size(badge, 50, 32); + lv_obj_set_style_radius(badge, 10, 0); + lv_obj_set_style_bg_opa(badge, LV_OPA_COVER, 0); + lv_obj_set_style_pad_all(badge, 0, 0); + + label = lv_label_create(badge); + lv_label_set_text(label, g_status_texts[id]); + lv_obj_set_width(label, LV_PCT(100)); + lv_obj_set_style_text_font(label, &ui_font_keyboard_small_18, 0); + lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_center(label); + + g_display_ui.status_badges[id] = badge; + g_display_ui.status_labels[id] = label; +} + +void display_ui_refresh_status_bar(const struct display_ui_model *model) +{ + for (uint32_t i = 0; i < DISPLAY_STATUS_COUNT; i++) { + lv_obj_t *badge = g_display_ui.status_badges[i]; + lv_obj_t *label = g_display_ui.status_labels[i]; + bool active = display_ui_status_is_active((enum display_status_id)i, model); + + if (!badge || !label) { + continue; + } + + lv_obj_set_style_border_width(badge, 4, 0); + lv_obj_set_style_border_color(badge, + active ? model->theme_color : + model->inactive_border_color, + 0); + lv_obj_set_style_bg_color(badge, + active ? lv_color_hex(0x1D2735) : + lv_color_hex(0x161A20), + 0); + lv_obj_set_style_text_color(label, + active ? lv_color_white() : + lv_color_hex(0x7C8798), + 0); + } +} + +void display_ui_refresh_battery(const struct display_ui_model *model) +{ + char battery_text[8]; + lv_color_t battery_color; + const char *state_symbol = ""; + lv_color_t state_color = lv_color_white(); + + if (!g_display_ui.battery_icon || + !g_display_ui.battery_label || + !g_display_ui.battery_state_label) { + return; + } + + battery_color = display_ui_get_battery_color(model->battery_level); + snprintk(battery_text, sizeof(battery_text), "%u%%", model->battery_level); + + if ((model->battery_flags & BATTERY_STATUS_FLAG_FULL) != 0U) { + state_symbol = DISPLAY_SYMBOL_PLUG; + state_color = lv_color_hex(0x4C9EF5); + } else if ((model->battery_flags & BATTERY_STATUS_FLAG_CHARGING) != 0U) { + state_symbol = LV_SYMBOL_CHARGE; + state_color = lv_color_hex(0xF4D35E); + } + + lv_label_set_text(g_display_ui.battery_icon, + display_ui_get_battery_symbol(model->battery_level)); + lv_obj_set_style_text_color(g_display_ui.battery_icon, battery_color, 0); + lv_label_set_text(g_display_ui.battery_label, battery_text); + lv_label_set_text(g_display_ui.battery_state_label, state_symbol); + lv_obj_set_style_text_color(g_display_ui.battery_state_label, state_color, 0); +} + +void display_ui_refresh_datetime(const char *date_text, const char *time_text) +{ + if (!g_display_ui.date_label || !g_display_ui.time_label) { + return; + } + + lv_label_set_text(g_display_ui.date_label, date_text); + lv_label_set_text(g_display_ui.time_label, time_text); +} + +void display_ui_refresh_all(const struct display_ui_model *model, + const char *date_text, + const char *time_text) +{ + display_ui_refresh_status_bar(model); + display_ui_refresh_battery(model); + display_ui_refresh_datetime(date_text, time_text); +} + +void display_ui_init(const struct display_ui_model *model, + const char *date_text, + const char *time_text) +{ + lv_obj_t *screen = lv_screen_active(); + lv_obj_t *content; + lv_obj_t *top_row; + lv_obj_t *battery_wrap; + lv_obj_t *middle_row; + lv_obj_t *bottom_row; + + memset(&g_display_ui, 0, sizeof(g_display_ui)); + + lv_obj_clean(screen); + lv_obj_set_style_bg_color(screen, lv_color_hex(0x0F1115), 0); + lv_obj_set_style_bg_grad_color(screen, lv_color_hex(0x1A1F29), 0); + lv_obj_set_style_bg_grad_dir(screen, LV_GRAD_DIR_VER, 0); + lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0); + lv_obj_set_style_text_color(screen, lv_color_white(), 0); + lv_obj_set_style_pad_all(screen, 0, 0); + lv_obj_set_scrollbar_mode(screen, LV_SCROLLBAR_MODE_OFF); + + content = lv_obj_create(screen); + lv_obj_remove_style_all(content); + lv_obj_set_size(content, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_bg_color(content, lv_color_hex(0x0F1115), 0); + lv_obj_set_style_bg_opa(content, LV_OPA_TRANSP, 0); + lv_obj_set_style_pad_left(content, 14, 0); + lv_obj_set_style_pad_right(content, 14, 0); + lv_obj_set_style_pad_top(content, 8, 0); + lv_obj_set_style_pad_bottom(content, 8, 0); + lv_obj_set_layout(content, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(content, + LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); + + top_row = lv_obj_create(content); + lv_obj_remove_style_all(top_row); + lv_obj_set_width(top_row, LV_PCT(100)); + lv_obj_set_flex_grow(top_row, 1); + lv_obj_set_style_bg_color(top_row, lv_color_hex(0x0F1115), 0); + lv_obj_set_style_bg_opa(top_row, LV_OPA_TRANSP, 0); + lv_obj_set_layout(top_row, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(top_row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(top_row, + LV_FLEX_ALIGN_SPACE_BETWEEN, + LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); + + g_display_ui.date_label = lv_label_create(top_row); + lv_obj_set_style_text_font(g_display_ui.date_label, &ui_font_keyboard_small_18, 0); + lv_obj_set_style_text_color(g_display_ui.date_label, lv_color_hex(0xD8DEE9), 0); + + battery_wrap = lv_obj_create(top_row); + lv_obj_remove_style_all(battery_wrap); + lv_obj_set_width(battery_wrap, LV_SIZE_CONTENT); + lv_obj_set_layout(battery_wrap, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(battery_wrap, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(battery_wrap, + LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(battery_wrap, 4, 0); + + g_display_ui.battery_icon = lv_label_create(battery_wrap); + lv_obj_set_style_text_font(g_display_ui.battery_icon, &ui_font_keyboard_small_18, 0); + + g_display_ui.battery_label = lv_label_create(battery_wrap); + lv_obj_set_style_text_font(g_display_ui.battery_label, &ui_font_keyboard_small_18, 0); + lv_obj_set_style_text_color(g_display_ui.battery_label, lv_color_hex(0xD8DEE9), 0); + + g_display_ui.battery_state_label = lv_label_create(battery_wrap); + lv_obj_set_style_text_font(g_display_ui.battery_state_label, &ui_font_keyboard_small_18, 0); + + middle_row = lv_obj_create(content); + lv_obj_remove_style_all(middle_row); + lv_obj_set_width(middle_row, LV_PCT(100)); + lv_obj_set_flex_grow(middle_row, 2); + lv_obj_set_style_bg_color(middle_row, lv_color_hex(0x0F1115), 0); + lv_obj_set_style_bg_opa(middle_row, LV_OPA_TRANSP, 0); + + g_display_ui.time_label = lv_label_create(middle_row); + lv_obj_set_style_text_font(g_display_ui.time_label, &ui_font_keyboard_time_48, 0); + lv_obj_set_style_text_color(g_display_ui.time_label, lv_color_white(), 0); + lv_obj_center(g_display_ui.time_label); + + bottom_row = lv_obj_create(content); + lv_obj_remove_style_all(bottom_row); + lv_obj_set_width(bottom_row, LV_PCT(100)); + lv_obj_set_flex_grow(bottom_row, 1); + lv_obj_set_style_bg_color(bottom_row, lv_color_hex(0x0F1115), 0); + lv_obj_set_style_bg_opa(bottom_row, LV_OPA_TRANSP, 0); + lv_obj_set_layout(bottom_row, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(bottom_row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(bottom_row, + LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER, + LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(bottom_row, 6, 0); + + for (uint32_t i = 0; i < DISPLAY_STATUS_COUNT; i++) { + display_ui_create_status_chip(bottom_row, (enum display_status_id)i); + } + + display_ui_refresh_all(model, date_text, time_text); +} diff --git a/src/ui/display_ui.h b/src/ui/display_ui.h new file mode 100644 index 0000000..5d01712 --- /dev/null +++ b/src/ui/display_ui.h @@ -0,0 +1,34 @@ +#ifndef DISPLAY_UI_H +#define DISPLAY_UI_H + +#include + +#include + +#include "mode_event.h" + +struct display_ui_model +{ + lv_color_t theme_color; + lv_color_t inactive_border_color; + uint8_t battery_level; + mode_type_t mode; + uint8_t led_mask; + uint8_t battery_flags; +}; + +void display_ui_init(const struct display_ui_model *model, + const char *date_text, + const char *time_text); + +void display_ui_refresh_all(const struct display_ui_model *model, + const char *date_text, + const char *time_text); + +void display_ui_refresh_status_bar(const struct display_ui_model *model); + +void display_ui_refresh_battery(const struct display_ui_model *model); + +void display_ui_refresh_datetime(const char *date_text, const char *time_text); + +#endif /* DISPLAY_UI_H */