feat(ui): 重构设置界面为页面控制器架构

- 添加新的UI页面基础架构(ui_page.h),包含页面操作接口
- 创建设置页面控制器(ui_settings_controller.h/.c)来管理页面导航
- 实现具体的设置页面类型:根页面、BLE页面、主题页面
- 修改display_module.c以使用新的页面系统替代旧的状态机
- 移除过时的settings_ui.h头文件和相关状态结构
- 更新事件处理逻辑以使用页面指针而非状态数据传递
- 修改主界面实现以适配统一的页面接口标准
This commit is contained in:
2026-04-23 18:46:55 +08:00
parent fbdc5426be
commit 48968e7880
18 changed files with 828 additions and 625 deletions

View File

@@ -10,53 +10,22 @@
#include <caf/events/power_event.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/printk.h>
#include "encoder_event.h"
#include "module_lifecycle.h"
#include "settings_mode_event.h"
#include "settings_ui.h"
#include "settings_view_event.h"
#include "theme_color.h"
#include "theme_rgb_update_event.h"
#include "ui/ui_settings_controller.h"
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
#define SETTINGS_MUTE_KEY_ID 0x180U
enum root_menu_item {
ROOT_MENU_ITEM_BLUETOOTH = 0,
ROOT_MENU_ITEM_THEME,
};
enum ble_menu_item {
BLE_MENU_ITEM_SLOT_1 = 0,
BLE_MENU_ITEM_SLOT_2,
BLE_MENU_ITEM_SLOT_3,
BLE_MENU_ITEM_ERASE_BOND,
};
struct named_theme {
const char *name;
struct theme_rgb color;
};
struct ble_slot_state {
char label[SETTINGS_UI_VALUE_MAX];
};
struct settings_module_ctx {
struct module_lifecycle_ctx lc;
bool active;
enum settings_ui_page page;
uint8_t root_selected;
uint8_t ble_selected;
uint8_t theme_selected;
uint8_t active_ble_slot;
struct ble_slot_state ble_slots[SETTINGS_UI_BLE_SLOT_COUNT];
struct theme_rgb current_theme;
uint8_t current_theme_index;
bool theme_matches_palette;
};
static int do_init(void);
@@ -74,37 +43,6 @@ static const struct module_lifecycle_ops lifecycle_ops = {
.do_stop = do_stop,
};
static const struct named_theme theme_palette[SETTINGS_UI_THEME_OPTION_COUNT] = {
{
.name = "Red",
.color = { .r = 0xFF, .g = 0x00, .b = 0x00 },
},
{
.name = "Amber",
.color = { .r = 0xFF, .g = 0x95, .b = 0x00 },
},
{
.name = "Default",
.color = {
.r = BLINKY_THEME_DEFAULT_R,
.g = BLINKY_THEME_DEFAULT_G,
.b = BLINKY_THEME_DEFAULT_B,
},
},
{
.name = "Green",
.color = { .r = 0x34, .g = 0xC7, .b = 0x59 },
},
{
.name = "Purple",
.color = { .r = 0xBF, .g = 0x5A, .b = 0xF2 },
},
{
.name = "White",
.color = { .r = 0xF2, .g = 0xF2, .b = 0xF7 },
},
};
static struct settings_module_ctx ctx = {
.lc = {
.state = LC_UNINIT,
@@ -113,117 +51,6 @@ static struct settings_module_ctx ctx = {
},
};
static bool theme_equal(const struct theme_rgb *lhs, const struct theme_rgb *rhs)
{
return (lhs->r == rhs->r) && (lhs->g == rhs->g) && (lhs->b == rhs->b);
}
static uint8_t wrap_index(uint8_t current, uint8_t count, int8_t delta)
{
int32_t next = current;
if (count == 0U) {
return 0U;
}
next += delta;
while (next < 0) {
next += count;
}
return (uint8_t)(next % count);
}
static void set_text(char *dst, size_t dst_size, const char *src)
{
if ((dst == NULL) || (dst_size == 0U) || (src == NULL)) {
return;
}
strncpy(dst, src, dst_size);
dst[dst_size - 1U] = '\0';
}
static const char *current_theme_name_get(void)
{
if (!ctx.theme_matches_palette) {
return "Custom";
}
return theme_palette[ctx.current_theme_index].name;
}
static void update_theme_match_state(void)
{
ctx.theme_matches_palette = false;
ctx.current_theme_index = 0U;
for (uint8_t i = 0; i < SETTINGS_UI_THEME_OPTION_COUNT; i++) {
if (!theme_equal(&ctx.current_theme, &theme_palette[i].color)) {
continue;
}
ctx.current_theme_index = i;
ctx.theme_matches_palette = true;
return;
}
}
static void publish_view_state(void)
{
struct settings_ui_state state = {
.active = ctx.active,
.page = ctx.page,
.root_selected = ctx.root_selected,
.ble_selected = ctx.ble_selected,
.theme_selected = ctx.theme_selected,
.active_ble_slot = ctx.active_ble_slot,
.accent = ctx.current_theme,
.theme_option_count = SETTINGS_UI_THEME_OPTION_COUNT,
};
set_text(state.root_items[ROOT_MENU_ITEM_BLUETOOTH].title,
sizeof(state.root_items[ROOT_MENU_ITEM_BLUETOOTH].title),
"Bluetooth");
set_text(state.root_items[ROOT_MENU_ITEM_BLUETOOTH].value,
sizeof(state.root_items[ROOT_MENU_ITEM_BLUETOOTH].value),
(ctx.active_ble_slot == 0U) ? "Slot 1" :
(ctx.active_ble_slot == 1U) ? "Slot 2" : "Slot 3");
set_text(state.root_items[ROOT_MENU_ITEM_THEME].title,
sizeof(state.root_items[ROOT_MENU_ITEM_THEME].title), "Theme");
set_text(state.root_items[ROOT_MENU_ITEM_THEME].value,
sizeof(state.root_items[ROOT_MENU_ITEM_THEME].value),
current_theme_name_get());
for (uint8_t i = 0; i < SETTINGS_UI_BLE_SLOT_COUNT; i++) {
char slot_name[SETTINGS_UI_TITLE_MAX];
snprintk(slot_name, sizeof(slot_name), "Slot %u", i + 1U);
set_text(state.ble_items[i].title, sizeof(state.ble_items[i].title),
slot_name);
set_text(state.ble_items[i].value, sizeof(state.ble_items[i].value),
ctx.ble_slots[i].label);
}
set_text(state.ble_items[BLE_MENU_ITEM_ERASE_BOND].title,
sizeof(state.ble_items[BLE_MENU_ITEM_ERASE_BOND].title),
"Erase Bond");
set_text(state.ble_items[BLE_MENU_ITEM_ERASE_BOND].value,
sizeof(state.ble_items[BLE_MENU_ITEM_ERASE_BOND].value),
(ctx.active_ble_slot == 0U) ? "Slot 1" :
(ctx.active_ble_slot == 1U) ? "Slot 2" : "Slot 3");
for (uint8_t i = 0; i < SETTINGS_UI_THEME_OPTION_COUNT; i++) {
set_text(state.theme_options[i].name,
sizeof(state.theme_options[i].name),
theme_palette[i].name);
state.theme_options[i].color = theme_palette[i].color;
}
submit_settings_view_event(&state);
}
static void settings_exit(void)
{
if (!ctx.active) {
@@ -231,9 +58,8 @@ static void settings_exit(void)
}
ctx.active = false;
ctx.page = SETTINGS_UI_PAGE_ROOT;
ui_settings_controller_close();
submit_settings_mode_event(false);
publish_view_state();
}
static void settings_enter(void)
@@ -243,139 +69,19 @@ static void settings_enter(void)
}
ctx.active = true;
ctx.page = SETTINGS_UI_PAGE_ROOT;
ctx.root_selected = ROOT_MENU_ITEM_BLUETOOTH;
ctx.ble_selected = ctx.active_ble_slot;
ctx.theme_selected = ctx.current_theme_index;
submit_settings_mode_event(true);
publish_view_state();
}
static void apply_theme_selection(uint8_t index)
{
if (index >= SETTINGS_UI_THEME_OPTION_COUNT) {
return;
}
ctx.theme_selected = index;
ctx.current_theme_index = index;
ctx.current_theme = theme_palette[index].color;
ctx.theme_matches_palette = true;
submit_theme_rgb_update_event(ctx.current_theme);
}
static void handle_confirm(void)
{
switch (ctx.page) {
case SETTINGS_UI_PAGE_ROOT:
ctx.page = (ctx.root_selected == ROOT_MENU_ITEM_BLUETOOTH) ?
SETTINGS_UI_PAGE_BLE :
SETTINGS_UI_PAGE_THEME;
ctx.ble_selected = ctx.active_ble_slot;
ctx.theme_selected = ctx.current_theme_index;
publish_view_state();
break;
case SETTINGS_UI_PAGE_BLE:
if (ctx.ble_selected < SETTINGS_UI_BLE_SLOT_COUNT) {
ctx.active_ble_slot = ctx.ble_selected;
} else {
set_text(ctx.ble_slots[ctx.active_ble_slot].label,
sizeof(ctx.ble_slots[ctx.active_ble_slot].label),
"Empty");
}
ctx.page = SETTINGS_UI_PAGE_ROOT;
ctx.root_selected = ROOT_MENU_ITEM_BLUETOOTH;
publish_view_state();
break;
case SETTINGS_UI_PAGE_THEME:
apply_theme_selection(ctx.theme_selected);
ctx.page = SETTINGS_UI_PAGE_ROOT;
ctx.root_selected = ROOT_MENU_ITEM_THEME;
publish_view_state();
break;
default:
break;
}
}
static void handle_back(void)
{
switch (ctx.page) {
case SETTINGS_UI_PAGE_ROOT:
settings_exit();
break;
case SETTINGS_UI_PAGE_BLE:
ctx.page = SETTINGS_UI_PAGE_ROOT;
ctx.root_selected = ROOT_MENU_ITEM_BLUETOOTH;
publish_view_state();
break;
case SETTINGS_UI_PAGE_THEME:
ctx.page = SETTINGS_UI_PAGE_ROOT;
ctx.root_selected = ROOT_MENU_ITEM_THEME;
publish_view_state();
break;
default:
break;
}
}
static void handle_navigation(int8_t detents)
{
if (detents == 0) {
return;
}
switch (ctx.page) {
case SETTINGS_UI_PAGE_ROOT:
ctx.root_selected = wrap_index(ctx.root_selected,
SETTINGS_UI_ROOT_ITEM_COUNT,
detents);
publish_view_state();
break;
case SETTINGS_UI_PAGE_BLE:
ctx.ble_selected = wrap_index(ctx.ble_selected,
SETTINGS_UI_BLE_ITEM_COUNT,
detents);
publish_view_state();
break;
case SETTINGS_UI_PAGE_THEME:
ctx.theme_selected = wrap_index(ctx.theme_selected,
SETTINGS_UI_THEME_OPTION_COUNT,
detents);
publish_view_state();
break;
default:
break;
}
ui_settings_controller_open();
}
static int do_init(void)
{
ctx.active = false;
ctx.page = SETTINGS_UI_PAGE_ROOT;
ctx.root_selected = ROOT_MENU_ITEM_BLUETOOTH;
ctx.ble_selected = BLE_MENU_ITEM_SLOT_1;
ctx.theme_selected = 0U;
ctx.active_ble_slot = 0U;
ctx.current_theme = (struct theme_rgb) {
.r = BLINKY_THEME_DEFAULT_R,
.g = BLINKY_THEME_DEFAULT_G,
.b = BLINKY_THEME_DEFAULT_B,
};
update_theme_match_state();
set_text(ctx.ble_slots[0].label, sizeof(ctx.ble_slots[0].label), "Host A");
set_text(ctx.ble_slots[1].label, sizeof(ctx.ble_slots[1].label), "Host B");
set_text(ctx.ble_slots[2].label, sizeof(ctx.ble_slots[2].label), "Empty");
ui_settings_theme_set_current(ctx.current_theme);
return 0;
}
@@ -408,11 +114,17 @@ static bool handle_click_event(const struct click_event *event)
switch (event->click) {
case CLICK_SHORT:
handle_confirm();
ui_settings_controller_select();
break;
case CLICK_LONG:
handle_back();
{
bool active = ui_settings_controller_back();
if (!active) {
settings_exit();
}
}
break;
default:
@@ -428,7 +140,7 @@ static bool handle_encoder_event(const struct encoder_event *event)
return false;
}
handle_navigation(event->detents);
ui_settings_controller_move(event->detents);
return false;
}
@@ -436,14 +148,10 @@ static bool handle_theme_rgb_update_event(
const struct theme_rgb_update_event *event)
{
ctx.current_theme = event->theme;
update_theme_match_state();
if (ctx.page == SETTINGS_UI_PAGE_THEME) {
ctx.theme_selected = ctx.current_theme_index;
}
ui_settings_theme_set_current(event->theme);
if (ctx.active) {
publish_view_state();
ui_settings_controller_refresh(false);
}
return false;