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

@@ -38,6 +38,10 @@ target_sources(app PRIVATE
src/settings_module.c src/settings_module.c
src/ui/ui_main.c src/ui/ui_main.c
src/ui/ui_settings.c src/ui/ui_settings.c
src/ui/ui_settings_controller.c
src/ui/ui_settings_root.c
src/ui/ui_settings_ble.c
src/ui/ui_settings_theme.c
src/protocol_module.c src/protocol_module.c
src/usb_cdc_module.c src/usb_cdc_module.c
src/usb_device_module.c src/usb_device_module.c

View File

@@ -1,12 +1,10 @@
#ifndef BLINKY_SETTINGS_VIEW_EVENT_H_ #ifndef BLINKY_SETTINGS_VIEW_EVENT_H_
#define BLINKY_SETTINGS_VIEW_EVENT_H_ #define BLINKY_SETTINGS_VIEW_EVENT_H_
#include <string.h>
#include <app_event_manager.h> #include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h> #include <app_event_manager_profiler_tracer.h>
#include "settings_ui.h" #include "ui/ui_settings_page.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@@ -14,22 +12,19 @@ extern "C" {
struct settings_view_event { struct settings_view_event {
struct app_event_header header; struct app_event_header header;
struct settings_ui_state state; struct ui_settings_page *page;
bool animate;
}; };
APP_EVENT_TYPE_DECLARE(settings_view_event); APP_EVENT_TYPE_DECLARE(settings_view_event);
static inline void submit_settings_view_event( static inline void submit_settings_view_event(struct ui_settings_page *page,
const struct settings_ui_state *state) bool animate)
{ {
struct settings_view_event *event; struct settings_view_event *event = new_settings_view_event();
if (state == NULL) { event->page = page;
return; event->animate = animate;
}
event = new_settings_view_event();
memcpy(&event->state, state, sizeof(event->state));
APP_EVENT_SUBMIT(event); APP_EVENT_SUBMIT(event);
} }

View File

@@ -1,56 +0,0 @@
#ifndef BLINKY_SETTINGS_UI_H_
#define BLINKY_SETTINGS_UI_H_
#include <stdbool.h>
#include <stdint.h>
#include "theme_color.h"
#ifdef __cplusplus
extern "C" {
#endif
#define SETTINGS_UI_ROOT_ITEM_COUNT 2U
#define SETTINGS_UI_BLE_SLOT_COUNT 3U
#define SETTINGS_UI_BLE_ITEM_COUNT 4U
#define SETTINGS_UI_THEME_OPTION_COUNT 6U
#define SETTINGS_UI_TITLE_MAX 20U
#define SETTINGS_UI_VALUE_MAX 24U
#define SETTINGS_UI_THEME_NAME_MAX 16U
enum settings_ui_page {
SETTINGS_UI_PAGE_ROOT = 0,
SETTINGS_UI_PAGE_BLE,
SETTINGS_UI_PAGE_THEME,
};
struct settings_ui_list_item {
char title[SETTINGS_UI_TITLE_MAX];
char value[SETTINGS_UI_VALUE_MAX];
};
struct settings_ui_theme_option {
char name[SETTINGS_UI_THEME_NAME_MAX];
struct theme_rgb color;
};
struct settings_ui_state {
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 theme_rgb accent;
struct settings_ui_list_item root_items[SETTINGS_UI_ROOT_ITEM_COUNT];
struct settings_ui_list_item ble_items[SETTINGS_UI_BLE_ITEM_COUNT];
struct settings_ui_theme_option
theme_options[SETTINGS_UI_THEME_OPTION_COUNT];
uint8_t theme_option_count;
};
#ifdef __cplusplus
}
#endif
#endif /* BLINKY_SETTINGS_UI_H_ */

66
inc/ui/ui_page.h Normal file
View File

@@ -0,0 +1,66 @@
#ifndef BLINKY_UI_PAGE_H_
#define BLINKY_UI_PAGE_H_
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
struct ui_page;
struct ui_page_ops {
void (*init)(struct ui_page *page);
void (*deinit)(struct ui_page *page);
void (*refresh)(struct ui_page *page);
};
struct ui_page {
const struct ui_page_ops *ops;
struct ui_page *parent;
bool initialized;
};
static inline void ui_page_init(struct ui_page *page)
{
if ((page == NULL) || (page->ops == NULL)) {
return;
}
if (page->initialized) {
return;
}
if (page->ops->init != NULL) {
page->ops->init(page);
}
page->initialized = true;
}
static inline void ui_page_deinit(struct ui_page *page)
{
if ((page == NULL) || !page->initialized || (page->ops == NULL)) {
return;
}
if (page->ops->deinit != NULL) {
page->ops->deinit(page);
}
page->initialized = false;
}
static inline void ui_page_refresh(struct ui_page *page)
{
if ((page == NULL) || !page->initialized ||
(page->ops == NULL) || (page->ops->refresh == NULL)) {
return;
}
page->ops->refresh(page);
}
#ifdef __cplusplus
}
#endif
#endif /* BLINKY_UI_PAGE_H_ */

View File

@@ -0,0 +1,38 @@
#ifndef BLINKY_UI_SETTINGS_CONTROLLER_H_
#define BLINKY_UI_SETTINGS_CONTROLLER_H_
#include <stdbool.h>
#include <stdint.h>
#include "theme_color.h"
#include "ui_page.h"
#include "ui_settings_page.h"
#ifdef __cplusplus
extern "C" {
#endif
void ui_settings_controller_open(void);
void ui_settings_controller_close(void);
bool ui_settings_controller_back(void);
void ui_settings_controller_select(void);
void ui_settings_controller_move(int8_t delta);
void ui_settings_controller_refresh(bool animate);
bool ui_settings_controller_is_active(void);
void ui_settings_controller_switch_to(struct ui_settings_page *page,
struct ui_page *parent);
const char *ui_settings_ble_current_label(void);
const char *ui_settings_ble_slot_label(uint8_t slot);
void ui_settings_ble_select_slot(uint8_t slot);
void ui_settings_ble_erase_current(void);
void ui_settings_theme_set_current(struct theme_rgb theme);
const char *ui_settings_theme_current_name(void);
uint8_t ui_settings_theme_current_index(void);
#ifdef __cplusplus
}
#endif
#endif /* BLINKY_UI_SETTINGS_CONTROLLER_H_ */

56
inc/ui/ui_settings_page.h Normal file
View File

@@ -0,0 +1,56 @@
#ifndef BLINKY_UI_SETTINGS_PAGE_H_
#define BLINKY_UI_SETTINGS_PAGE_H_
#include <stdint.h>
#include <lvgl.h>
#include "ui_page.h"
#ifdef __cplusplus
extern "C" {
#endif
struct ui_settings_page;
struct ui_settings_item;
typedef void (*ui_settings_item_draw_fn)(const struct ui_settings_item *item,
lv_obj_t *container);
struct ui_settings_item {
const char *icon;
const char *title;
const char *value;
ui_settings_item_draw_fn draw;
void *user_data;
};
struct ui_settings_page_ops {
struct ui_page_ops base;
uint8_t (*get_count)(struct ui_settings_page *page);
void (*get_item)(struct ui_settings_page *page, uint8_t index,
struct ui_settings_item *item);
void (*on_enter)(struct ui_settings_page *page);
void (*on_select)(struct ui_settings_page *page, uint8_t index);
void (*on_back)(struct ui_settings_page *page);
};
struct ui_settings_page {
struct ui_page base;
const struct ui_settings_page_ops *ops;
const char *title;
const char *hint;
uint8_t selected;
};
static inline struct ui_settings_page *ui_page_to_settings(struct ui_page *page)
{
return (struct ui_settings_page *)page;
}
#ifdef __cplusplus
}
#endif
#endif /* BLINKY_UI_SETTINGS_PAGE_H_ */

View File

@@ -19,10 +19,10 @@
#include "module_lifecycle.h" #include "module_lifecycle.h"
#include "mode_switch_event.h" #include "mode_switch_event.h"
#include "settings_mode_event.h" #include "settings_mode_event.h"
#include "settings_ui.h"
#include "settings_view_event.h" #include "settings_view_event.h"
#include "theme_rgb_update_event.h" #include "theme_rgb_update_event.h"
#include "theme_color.h" #include "theme_color.h"
#include "ui/ui_page.h"
#include "ui/ui_main.h" #include "ui/ui_main.h"
#include "ui/ui_settings.h" #include "ui/ui_settings.h"
@@ -38,9 +38,8 @@ struct display_module_ctx {
const struct device *backlight_dev; const struct device *backlight_dev;
uint32_t backlight_idx; uint32_t backlight_idx;
struct ui_main_model ui_model; struct ui_main_model ui_model;
struct settings_ui_state settings_ui; struct ui_settings_page *settings_page;
bool settings_active; bool settings_active;
bool settings_ui_valid;
bool lvgl_initialized; bool lvgl_initialized;
char date_text[DATETIME_EVENT_DATE_TEXT_LEN]; char date_text[DATETIME_EVENT_DATE_TEXT_LEN];
char time_text[DATETIME_EVENT_TIME_TEXT_LEN]; char time_text[DATETIME_EVENT_TIME_TEXT_LEN];
@@ -81,6 +80,11 @@ static struct display_module_ctx ctx = {
.time_text = "00:00:00", .time_text = "00:00:00",
}; };
static struct ui_page *main_page(void)
{
return ui_main_page_get(&ctx.ui_model, ctx.date_text, ctx.time_text);
}
static int backlight_set(bool on) static int backlight_set(bool on)
{ {
if (on) { if (on) {
@@ -133,11 +137,16 @@ static int do_start(void)
ctx.lvgl_initialized = true; ctx.lvgl_initialized = true;
lvgl_lock(); lvgl_lock();
ui_main_init(&ctx.ui_model, ctx.date_text, ctx.time_text); ui_page_init(main_page());
ui_settings_init(NULL);
lvgl_unlock(); lvgl_unlock();
} }
lvgl_lock();
if (!ctx.settings_active) {
ui_page_init(main_page());
}
lvgl_unlock();
err = backlight_set(true); err = backlight_set(true);
if (err) { if (err) {
LOG_ERR("Backlight enable failed (%d)", err); LOG_ERR("Backlight enable failed (%d)", err);
@@ -175,15 +184,11 @@ static void refresh_ui(void)
return; return;
} }
if (ctx.settings_active && !ctx.settings_ui_valid) {
return;
}
lvgl_lock(); lvgl_lock();
if (ctx.settings_active) { if (ctx.settings_active && (ctx.settings_page != NULL)) {
ui_settings_refresh(&ctx.settings_ui, true); ui_settings_show(ctx.settings_page, true);
} else { } else {
ui_main_refresh_all(&ctx.ui_model, ctx.date_text, ctx.time_text); ui_page_refresh(main_page());
} }
lvgl_unlock(); lvgl_unlock();
} }
@@ -223,7 +228,6 @@ static bool app_event_handler(const struct app_event_header *aeh)
ctx.ui_model.theme_color = (lv_color_t)LV_COLOR_MAKE(event->theme.r, ctx.ui_model.theme_color = (lv_color_t)LV_COLOR_MAKE(event->theme.r,
event->theme.g, event->theme.g,
event->theme.b); event->theme.b);
ctx.settings_ui.accent = event->theme;
refresh_ui(); refresh_ui();
return false; return false;
} }
@@ -233,17 +237,24 @@ static bool app_event_handler(const struct app_event_header *aeh)
cast_settings_mode_event(aeh); cast_settings_mode_event(aeh);
ctx.settings_active = event->active; ctx.settings_active = event->active;
ctx.settings_ui_valid = false; if (!ctx.settings_active) {
ctx.settings_page = NULL;
}
if (!ctx.lvgl_initialized) { if (!ctx.lvgl_initialized) {
return false; return false;
} }
lvgl_lock(); lvgl_lock();
if (!ctx.settings_active) { if (ctx.settings_active) {
ui_settings_set_visible(false); ui_page_deinit(main_page());
ui_main_set_visible(true); ui_settings_init();
ui_main_refresh_all(&ctx.ui_model, ctx.date_text, if (ctx.settings_page != NULL) {
ctx.time_text); ui_settings_show(ctx.settings_page, false);
}
} else {
ui_settings_deinit();
ui_page_init(main_page());
ui_page_refresh(main_page());
} }
lvgl_unlock(); lvgl_unlock();
return false; return false;
@@ -253,18 +264,13 @@ static bool app_event_handler(const struct app_event_header *aeh)
const struct settings_view_event *event = const struct settings_view_event *event =
cast_settings_view_event(aeh); cast_settings_view_event(aeh);
ctx.settings_ui = event->state; ctx.settings_page = event->page;
ctx.settings_ui_valid = true;
if (ctx.settings_active && ctx.lvgl_initialized) { if (ctx.settings_active && ctx.lvgl_initialized) {
lvgl_lock(); lvgl_lock();
ui_settings_refresh(&ctx.settings_ui, false); ui_settings_show(ctx.settings_page, event->animate);
ui_main_set_visible(false);
ui_settings_set_visible(true);
lvgl_unlock(); lvgl_unlock();
return false;
} }
refresh_ui();
return false; return false;
} }

View File

@@ -1,30 +1,14 @@
#include "settings_view_event.h" #include <stdint.h>
static const char *page_name(enum settings_ui_page page) #include "settings_view_event.h"
{
switch (page) {
case SETTINGS_UI_PAGE_ROOT:
return "root";
case SETTINGS_UI_PAGE_BLE:
return "ble";
case SETTINGS_UI_PAGE_THEME:
return "theme";
default:
return "?";
}
}
static void log_settings_view_event(const struct app_event_header *aeh) static void log_settings_view_event(const struct app_event_header *aeh)
{ {
const struct settings_view_event *event = const struct settings_view_event *event =
cast_settings_view_event(aeh); cast_settings_view_event(aeh);
APP_EVENT_MANAGER_LOG( APP_EVENT_MANAGER_LOG(aeh, "page:%p animate:%u",
aeh, event->page, event->animate);
"active:%u page:%s root_sel:%u ble_sel:%u theme_sel:%u slot:%u",
event->state.active, page_name(event->state.page),
event->state.root_selected, event->state.ble_selected,
event->state.theme_selected, event->state.active_ble_slot);
} }
static void profile_settings_view_event(struct log_event_buf *buf, static void profile_settings_view_event(struct log_event_buf *buf,
@@ -33,23 +17,13 @@ static void profile_settings_view_event(struct log_event_buf *buf,
const struct settings_view_event *event = const struct settings_view_event *event =
cast_settings_view_event(aeh); cast_settings_view_event(aeh);
nrf_profiler_log_encode_uint8(buf, event->state.active ? 1U : 0U); nrf_profiler_log_encode_uint32(buf, (uint32_t)(uintptr_t)event->page);
nrf_profiler_log_encode_uint8(buf, event->state.page); nrf_profiler_log_encode_uint8(buf, event->animate ? 1U : 0U);
nrf_profiler_log_encode_uint8(buf, event->state.root_selected);
nrf_profiler_log_encode_uint8(buf, event->state.ble_selected);
nrf_profiler_log_encode_uint8(buf, event->state.theme_selected);
nrf_profiler_log_encode_uint8(buf, event->state.active_ble_slot);
} }
APP_EVENT_INFO_DEFINE(settings_view_event, APP_EVENT_INFO_DEFINE(settings_view_event,
ENCODE(NRF_PROFILER_ARG_U8, ENCODE(NRF_PROFILER_ARG_U32, NRF_PROFILER_ARG_U8),
NRF_PROFILER_ARG_U8, ENCODE("page", "animate"),
NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8),
ENCODE("active", "page", "root_selected", "ble_selected",
"theme_selected", "active_ble_slot"),
profile_settings_view_event); profile_settings_view_event);
APP_EVENT_TYPE_DEFINE(settings_view_event, APP_EVENT_TYPE_DEFINE(settings_view_event,

View File

@@ -10,53 +10,22 @@
#include <caf/events/power_event.h> #include <caf/events/power_event.h>
#include <zephyr/logging/log.h> #include <zephyr/logging/log.h>
#include <zephyr/sys/printk.h>
#include "encoder_event.h" #include "encoder_event.h"
#include "module_lifecycle.h" #include "module_lifecycle.h"
#include "settings_mode_event.h" #include "settings_mode_event.h"
#include "settings_ui.h"
#include "settings_view_event.h"
#include "theme_color.h" #include "theme_color.h"
#include "theme_rgb_update_event.h" #include "theme_rgb_update_event.h"
#include "ui/ui_settings_controller.h"
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF); LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
#define SETTINGS_MUTE_KEY_ID 0x180U #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 settings_module_ctx {
struct module_lifecycle_ctx lc; struct module_lifecycle_ctx lc;
bool active; 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; struct theme_rgb current_theme;
uint8_t current_theme_index;
bool theme_matches_palette;
}; };
static int do_init(void); static int do_init(void);
@@ -74,37 +43,6 @@ static const struct module_lifecycle_ops lifecycle_ops = {
.do_stop = do_stop, .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 = { static struct settings_module_ctx ctx = {
.lc = { .lc = {
.state = LC_UNINIT, .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) static void settings_exit(void)
{ {
if (!ctx.active) { if (!ctx.active) {
@@ -231,9 +58,8 @@ static void settings_exit(void)
} }
ctx.active = false; ctx.active = false;
ctx.page = SETTINGS_UI_PAGE_ROOT; ui_settings_controller_close();
submit_settings_mode_event(false); submit_settings_mode_event(false);
publish_view_state();
} }
static void settings_enter(void) static void settings_enter(void)
@@ -243,139 +69,19 @@ static void settings_enter(void)
} }
ctx.active = true; 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); submit_settings_mode_event(true);
publish_view_state(); ui_settings_controller_open();
}
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;
}
} }
static int do_init(void) static int do_init(void)
{ {
ctx.active = false; 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) { ctx.current_theme = (struct theme_rgb) {
.r = BLINKY_THEME_DEFAULT_R, .r = BLINKY_THEME_DEFAULT_R,
.g = BLINKY_THEME_DEFAULT_G, .g = BLINKY_THEME_DEFAULT_G,
.b = BLINKY_THEME_DEFAULT_B, .b = BLINKY_THEME_DEFAULT_B,
}; };
update_theme_match_state(); ui_settings_theme_set_current(ctx.current_theme);
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");
return 0; return 0;
} }
@@ -408,11 +114,17 @@ static bool handle_click_event(const struct click_event *event)
switch (event->click) { switch (event->click) {
case CLICK_SHORT: case CLICK_SHORT:
handle_confirm(); ui_settings_controller_select();
break; break;
case CLICK_LONG: case CLICK_LONG:
handle_back(); {
bool active = ui_settings_controller_back();
if (!active) {
settings_exit();
}
}
break; break;
default: default:
@@ -428,7 +140,7 @@ static bool handle_encoder_event(const struct encoder_event *event)
return false; return false;
} }
handle_navigation(event->detents); ui_settings_controller_move(event->detents);
return false; return false;
} }
@@ -436,14 +148,10 @@ static bool handle_theme_rgb_update_event(
const struct theme_rgb_update_event *event) const struct theme_rgb_update_event *event)
{ {
ctx.current_theme = event->theme; ctx.current_theme = event->theme;
update_theme_match_state(); ui_settings_theme_set_current(event->theme);
if (ctx.page == SETTINGS_UI_PAGE_THEME) {
ctx.theme_selected = ctx.current_theme_index;
}
if (ctx.active) { if (ctx.active) {
publish_view_state(); ui_settings_controller_refresh(false);
} }
return false; return false;

View File

@@ -31,6 +31,9 @@ struct ui_main_ctx {
static struct ui_main_ctx g_ui; static struct ui_main_ctx g_ui;
static bool ui_initialized; static bool ui_initialized;
static const struct ui_main_model *page_model;
static const char *page_date_text;
static const char *page_time_text;
static const char *const status_texts[UI_STATUS_COUNT] = { static const char *const status_texts[UI_STATUS_COUNT] = {
LV_SYMBOL_USB, LV_SYMBOL_USB,
@@ -297,15 +300,55 @@ void ui_main_init(const struct ui_main_model *model,
ui_initialized = true; ui_initialized = true;
} }
void ui_main_set_visible(bool visible) void ui_main_deinit(void)
{ {
if (!ui_initialized || (g_ui.content == NULL)) { if (!ui_initialized) {
return; return;
} }
if (visible) { if (g_ui.content != NULL) {
lv_obj_clear_flag(g_ui.content, LV_OBJ_FLAG_HIDDEN); lv_obj_delete(g_ui.content);
} else {
lv_obj_add_flag(g_ui.content, LV_OBJ_FLAG_HIDDEN);
} }
memset(&g_ui, 0, sizeof(g_ui));
ui_initialized = false;
}
static void main_page_init(struct ui_page *page)
{
ARG_UNUSED(page);
ui_main_init(page_model, page_date_text, page_time_text);
}
static void main_page_deinit(struct ui_page *page)
{
ARG_UNUSED(page);
ui_main_deinit();
}
static void main_page_refresh(struct ui_page *page)
{
ARG_UNUSED(page);
ui_main_refresh_all(page_model, page_date_text, page_time_text);
}
static const struct ui_page_ops main_page_ops = {
.init = main_page_init,
.deinit = main_page_deinit,
.refresh = main_page_refresh,
};
static struct ui_page main_page = {
.ops = &main_page_ops,
};
struct ui_page *ui_main_page_get(const struct ui_main_model *model,
const char *date_text,
const char *time_text)
{
page_model = model;
page_date_text = date_text;
page_time_text = time_text;
return &main_page;
} }

View File

@@ -7,6 +7,7 @@
#include <lvgl.h> #include <lvgl.h>
#include "mode_switch_event.h" #include "mode_switch_event.h"
#include "ui/ui_page.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@@ -31,7 +32,10 @@ void ui_main_refresh_all(const struct ui_main_model *model,
void ui_main_refresh_status_bar(const struct ui_main_model *model); void ui_main_refresh_status_bar(const struct ui_main_model *model);
void ui_main_refresh_battery(const struct ui_main_model *model); void ui_main_refresh_battery(const struct ui_main_model *model);
void ui_main_refresh_datetime(const char *date_text, const char *time_text); void ui_main_refresh_datetime(const char *date_text, const char *time_text);
void ui_main_set_visible(bool visible); void ui_main_deinit(void);
struct ui_page *ui_main_page_get(const struct ui_main_model *model,
const char *date_text,
const char *time_text);
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@@ -1,4 +1,3 @@
#include <stdio.h>
#include <string.h> #include <string.h>
#include <lvgl.h> #include <lvgl.h>
@@ -6,67 +5,41 @@
#include "ui_settings.h" #include "ui_settings.h"
#define UI_SETTINGS_LIST_H 104 #define UI_SETTINGS_LIST_H 104
#define UI_SETTINGS_ROOT_MAX SETTINGS_UI_ROOT_ITEM_COUNT #define UI_SETTINGS_MAX_ITEMS 8U
#define UI_SETTINGS_BLE_MAX SETTINGS_UI_BLE_ITEM_COUNT
#define UI_SETTINGS_THEME_MAX SETTINGS_UI_THEME_OPTION_COUNT
struct ui_settings_row { struct ui_settings_row {
lv_obj_t *row; lv_obj_t *row;
lv_obj_t *icon; lv_obj_t *icon;
lv_obj_t *swatch;
lv_obj_t *title; lv_obj_t *title;
lv_obj_t *value; lv_obj_t *value;
lv_obj_t *custom;
}; };
struct ui_settings_ctx { struct ui_settings_ctx {
lv_obj_t *content; lv_obj_t *content;
lv_obj_t *title; lv_obj_t *title;
lv_obj_t *hint; lv_obj_t *hint;
lv_obj_t *root_scroll; lv_obj_t *scroll;
lv_obj_t *ble_scroll;
lv_obj_t *theme_scroll;
struct ui_settings_row root_rows[UI_SETTINGS_ROOT_MAX];
struct ui_settings_row ble_rows[UI_SETTINGS_BLE_MAX];
struct ui_settings_row theme_rows[UI_SETTINGS_THEME_MAX];
lv_obj_t *value; lv_obj_t *value;
struct ui_settings_row rows[UI_SETTINGS_MAX_ITEMS];
lv_group_t *focus_group; lv_group_t *focus_group;
uint8_t row_count;
struct ui_settings_page *page;
bool initialized; bool initialized;
}; };
static struct ui_settings_ctx g_ui; static struct ui_settings_ctx g_ui;
static void scroll_set_visible(lv_obj_t *scroll, bool visible) static void row_clear_custom(struct ui_settings_row *row)
{ {
if (scroll == NULL) { lv_obj_clean(row->custom);
return; lv_obj_set_style_bg_opa(row->custom, LV_OPA_TRANSP, 0);
lv_obj_set_style_bg_color(row->custom, lv_color_hex(0x0B1017), 0);
} }
if (visible) { static void create_row(struct ui_settings_row *row)
lv_obj_clear_flag(scroll, LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(scroll, LV_OBJ_FLAG_HIDDEN);
}
}
static void create_scroll(lv_obj_t *parent, lv_obj_t **scroll)
{ {
*scroll = lv_obj_create(parent); row->row = lv_obj_create(g_ui.scroll);
lv_obj_remove_style_all(*scroll);
lv_obj_set_width(*scroll, LV_PCT(100));
lv_obj_set_height(*scroll, UI_SETTINGS_LIST_H);
lv_obj_set_style_bg_opa(*scroll, LV_OPA_TRANSP, 0);
lv_obj_set_layout(*scroll, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(*scroll, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(*scroll, LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_style_pad_row(*scroll, 6, 0);
lv_obj_set_scroll_dir(*scroll, LV_DIR_VER);
lv_obj_set_scrollbar_mode(*scroll, LV_SCROLLBAR_MODE_OFF);
}
static void create_row(lv_obj_t *parent, struct ui_settings_row *row)
{
row->row = lv_obj_create(parent);
lv_obj_remove_style_all(row->row); lv_obj_remove_style_all(row->row);
lv_obj_set_width(row->row, LV_PCT(100)); lv_obj_set_width(row->row, LV_PCT(100));
lv_obj_set_height(row->row, 30); lv_obj_set_height(row->row, 30);
@@ -88,12 +61,6 @@ static void create_row(lv_obj_t *parent, struct ui_settings_row *row)
lv_obj_set_style_text_font(row->icon, &lv_font_montserrat_14, 0); lv_obj_set_style_text_font(row->icon, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(row->icon, lv_color_hex(0x8A95A5), 0); lv_obj_set_style_text_color(row->icon, lv_color_hex(0x8A95A5), 0);
row->swatch = lv_obj_create(row->row);
lv_obj_remove_style_all(row->swatch);
lv_obj_set_size(row->swatch, 16, 16);
lv_obj_set_style_bg_opa(row->swatch, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(row->swatch, lv_color_hex(0x0B1017), 0);
row->title = lv_label_create(row->row); row->title = lv_label_create(row->row);
lv_obj_set_width(row->title, 118); lv_obj_set_width(row->title, 118);
lv_obj_set_style_text_font(row->title, &lv_font_montserrat_14, 0); lv_obj_set_style_text_font(row->title, &lv_font_montserrat_14, 0);
@@ -106,103 +73,69 @@ static void create_row(lv_obj_t *parent, struct ui_settings_row *row)
lv_obj_set_style_text_color(row->value, lv_color_hex(0x97A3B5), 0); lv_obj_set_style_text_color(row->value, lv_color_hex(0x97A3B5), 0);
lv_obj_set_style_text_align(row->value, LV_TEXT_ALIGN_RIGHT, 0); lv_obj_set_style_text_align(row->value, LV_TEXT_ALIGN_RIGHT, 0);
lv_label_set_long_mode(row->value, LV_LABEL_LONG_CLIP); lv_label_set_long_mode(row->value, LV_LABEL_LONG_CLIP);
row->custom = lv_obj_create(row->row);
lv_obj_remove_style_all(row->custom);
lv_obj_set_size(row->custom, 16, 16);
row_clear_custom(row);
} }
static void row_set(struct ui_settings_row *row, const char *icon, static void rebuild_rows(uint8_t count)
const char *title, const char *value, bool selected,
lv_color_t accent, const struct theme_rgb *swatch)
{ {
lv_label_set_text(row->icon, icon ? icon : ""); if (count > UI_SETTINGS_MAX_ITEMS) {
lv_label_set_text(row->title, title ? title : ""); count = UI_SETTINGS_MAX_ITEMS;
lv_label_set_text(row->value, value ? value : ""); }
if (g_ui.row_count == count) {
return;
}
lv_obj_clean(g_ui.scroll);
memset(g_ui.rows, 0, sizeof(g_ui.rows));
g_ui.row_count = count;
lv_group_delete(g_ui.focus_group);
g_ui.focus_group = lv_group_create();
for (uint8_t i = 0; i < g_ui.row_count; i++) {
create_row(&g_ui.rows[i]);
}
}
static void row_set(struct ui_settings_row *row,
const struct ui_settings_item *item,
bool selected,
lv_color_t accent)
{
lv_label_set_text(row->icon, item->icon ? item->icon : "");
lv_label_set_text(row->title, item->title ? item->title : "");
lv_label_set_text(row->value, item->value ? item->value : "");
lv_obj_set_style_border_color(row->row, lv_obj_set_style_border_color(row->row,
selected ? accent : lv_color_hex(0x0B1017), selected ? accent : lv_color_hex(0x0B1017),
0); 0);
lv_obj_set_style_text_color(row->icon, lv_color_hex(0x8A95A5), 0);
lv_obj_set_style_text_color(row->title, lv_color_hex(0xD8DEE9), 0);
lv_obj_set_style_text_color(row->value, lv_color_hex(0x97A3B5), 0);
if (swatch != NULL) { row_clear_custom(row);
lv_obj_set_style_bg_color(row->swatch, if (item->draw != NULL) {
lv_color_make(swatch->r, swatch->g, item->draw(item, row->custom);
swatch->b),
0);
} else {
lv_obj_set_style_bg_color(row->swatch, lv_color_hex(0x0B1017), 0);
} }
} }
static void focus_row(lv_obj_t *scroll, struct ui_settings_row *row, static void focus_row(uint8_t index, bool animate)
bool animate)
{ {
lv_obj_update_layout(scroll); if (index >= g_ui.row_count) {
lv_group_focus_obj(row->row); return;
lv_obj_scroll_to_view(row->row, animate ? LV_ANIM_ON : LV_ANIM_OFF);
} }
static void refresh_root(const struct settings_ui_state *state, lv_obj_update_layout(g_ui.scroll);
lv_color_t accent, bool animate) lv_group_focus_obj(g_ui.rows[index].row);
{ lv_obj_scroll_to_view(g_ui.rows[index].row,
for (uint8_t i = 0; i < UI_SETTINGS_ROOT_MAX; i++) { animate ? LV_ANIM_ON : LV_ANIM_OFF);
bool selected = state->root_selected == i;
const char *icon = (i == 0U) ? LV_SYMBOL_BLUETOOTH : LV_SYMBOL_TINT;
row_set(&g_ui.root_rows[i], icon,
state->root_items[i].title, state->root_items[i].value,
selected, accent, NULL);
} }
focus_row(g_ui.root_scroll, &g_ui.root_rows[state->root_selected], animate); void ui_settings_init(void)
}
static void refresh_ble(const struct settings_ui_state *state,
lv_color_t accent, bool animate)
{
for (uint8_t i = 0; i < UI_SETTINGS_BLE_MAX; i++) {
bool selected = state->ble_selected == i;
bool slot = i < SETTINGS_UI_BLE_SLOT_COUNT;
const char *icon = slot ? ((state->active_ble_slot == i) ?
LV_SYMBOL_OK : LV_SYMBOL_BLUETOOTH) :
LV_SYMBOL_TRASH;
row_set(&g_ui.ble_rows[i], icon,
state->ble_items[i].title, state->ble_items[i].value,
selected, accent, NULL);
}
focus_row(g_ui.ble_scroll, &g_ui.ble_rows[state->ble_selected], animate);
}
static void refresh_theme(const struct settings_ui_state *state,
lv_color_t accent, bool animate)
{
uint8_t count = state->theme_option_count;
uint8_t selected = state->theme_selected;
for (uint8_t i = 0; i < UI_SETTINGS_THEME_MAX; i++) {
bool selected_row = i == selected;
if (i < count) {
row_set(&g_ui.theme_rows[i], LV_SYMBOL_TINT,
state->theme_options[i].name, "", selected_row, accent,
&state->theme_options[i].color);
} else {
row_set(&g_ui.theme_rows[i], "", "", "", false, accent, NULL);
}
}
if (selected < count) {
focus_row(g_ui.theme_scroll, &g_ui.theme_rows[selected], animate);
}
}
void ui_settings_init(const struct settings_ui_state *state)
{ {
lv_obj_t *screen = lv_screen_active(); lv_obj_t *screen = lv_screen_active();
if (g_ui.initialized) { if (g_ui.initialized) {
ui_settings_refresh(state, false);
return; return;
} }
@@ -231,22 +164,19 @@ void ui_settings_init(const struct settings_ui_state *state)
g_ui.hint = lv_label_create(g_ui.content); g_ui.hint = lv_label_create(g_ui.content);
lv_obj_set_style_text_font(g_ui.hint, &lv_font_montserrat_14, 0); lv_obj_set_style_text_font(g_ui.hint, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(g_ui.hint, lv_color_hex(0x97A3B5), 0); lv_obj_set_style_text_color(g_ui.hint, lv_color_hex(0x97A3B5), 0);
lv_label_set_text(g_ui.hint, "Rotate select Tap OK Hold Back");
create_scroll(g_ui.content, &g_ui.root_scroll); g_ui.scroll = lv_obj_create(g_ui.content);
for (uint8_t i = 0; i < UI_SETTINGS_ROOT_MAX; i++) { lv_obj_remove_style_all(g_ui.scroll);
create_row(g_ui.root_scroll, &g_ui.root_rows[i]); lv_obj_set_width(g_ui.scroll, LV_PCT(100));
} lv_obj_set_height(g_ui.scroll, UI_SETTINGS_LIST_H);
lv_obj_set_style_bg_opa(g_ui.scroll, LV_OPA_TRANSP, 0);
create_scroll(g_ui.content, &g_ui.ble_scroll); lv_obj_set_layout(g_ui.scroll, LV_LAYOUT_FLEX);
for (uint8_t i = 0; i < UI_SETTINGS_BLE_MAX; i++) { lv_obj_set_flex_flow(g_ui.scroll, LV_FLEX_FLOW_COLUMN);
create_row(g_ui.ble_scroll, &g_ui.ble_rows[i]); lv_obj_set_flex_align(g_ui.scroll, LV_FLEX_ALIGN_START,
} LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_style_pad_row(g_ui.scroll, 6, 0);
create_scroll(g_ui.content, &g_ui.theme_scroll); lv_obj_set_scroll_dir(g_ui.scroll, LV_DIR_VER);
for (uint8_t i = 0; i < UI_SETTINGS_THEME_MAX; i++) { lv_obj_set_scrollbar_mode(g_ui.scroll, LV_SCROLLBAR_MODE_OFF);
create_row(g_ui.theme_scroll, &g_ui.theme_rows[i]);
}
g_ui.value = lv_label_create(g_ui.content); g_ui.value = lv_label_create(g_ui.content);
lv_obj_set_width(g_ui.value, LV_PCT(100)); lv_obj_set_width(g_ui.value, LV_PCT(100));
@@ -254,63 +184,50 @@ void ui_settings_init(const struct settings_ui_state *state)
lv_obj_set_style_text_color(g_ui.value, lv_color_hex(0x97A3B5), 0); lv_obj_set_style_text_color(g_ui.value, lv_color_hex(0x97A3B5), 0);
g_ui.initialized = true; g_ui.initialized = true;
ui_settings_set_visible(false);
if (state != NULL) {
ui_settings_refresh(state, false);
}
} }
void ui_settings_refresh(const struct settings_ui_state *state, bool animate) void ui_settings_show(struct ui_settings_page *page, bool animate)
{ {
lv_color_t accent; uint8_t count;
lv_color_t accent = lv_color_hex(0x4C9EF5);
if ((state == NULL) || !g_ui.initialized) { if ((page == NULL) || !g_ui.initialized ||
(page->ops == NULL) || (page->ops->get_count == NULL) ||
(page->ops->get_item == NULL)) {
return; return;
} }
accent = lv_color_make(state->accent.r, state->accent.g, state->accent.b); count = page->ops->get_count(page);
rebuild_rows(count);
g_ui.page = page;
scroll_set_visible(g_ui.root_scroll, false); lv_label_set_text(g_ui.title, page->title ? page->title : "");
scroll_set_visible(g_ui.ble_scroll, false); lv_label_set_text(g_ui.hint, page->hint ? page->hint : "");
scroll_set_visible(g_ui.theme_scroll, false); lv_label_set_text(g_ui.value, "");
switch (state->page) { for (uint8_t i = 0; i < g_ui.row_count; i++) {
case SETTINGS_UI_PAGE_ROOT: struct ui_settings_item item = { 0 };
scroll_set_visible(g_ui.root_scroll, true);
lv_label_set_text(g_ui.title, LV_SYMBOL_SETTINGS " Settings");
lv_label_set_text(g_ui.value, "Short: open Hold: exit");
refresh_root(state, accent, animate);
break;
case SETTINGS_UI_PAGE_BLE: page->ops->get_item(page, i, &item);
scroll_set_visible(g_ui.ble_scroll, true); row_set(&g_ui.rows[i], &item, page->selected == i, accent);
lv_label_set_text(g_ui.title, LV_SYMBOL_BLUETOOTH " Bluetooth");
lv_label_set_text(g_ui.value, "Short: select and return");
refresh_ble(state, accent, animate);
break;
case SETTINGS_UI_PAGE_THEME:
scroll_set_visible(g_ui.theme_scroll, true);
lv_label_set_text(g_ui.title, LV_SYMBOL_TINT " Theme");
lv_label_set_text(g_ui.value, "Short: apply and return");
refresh_theme(state, accent, animate);
break;
default:
break;
}
} }
void ui_settings_set_visible(bool visible) focus_row(page->selected, animate);
}
void ui_settings_deinit(void)
{ {
if (!g_ui.initialized || (g_ui.content == NULL)) { if (!g_ui.initialized) {
return; return;
} }
if (visible) { if (g_ui.focus_group != NULL) {
lv_obj_clear_flag(g_ui.content, LV_OBJ_FLAG_HIDDEN); lv_group_delete(g_ui.focus_group);
} else {
lv_obj_add_flag(g_ui.content, LV_OBJ_FLAG_HIDDEN);
} }
if (g_ui.content != NULL) {
lv_obj_delete(g_ui.content);
}
memset(&g_ui, 0, sizeof(g_ui));
} }

View File

@@ -1,15 +1,15 @@
#ifndef BLINKY_UI_SETTINGS_H_ #ifndef BLINKY_UI_SETTINGS_H_
#define BLINKY_UI_SETTINGS_H_ #define BLINKY_UI_SETTINGS_H_
#include "settings_ui.h" #include "ui/ui_settings_page.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
void ui_settings_init(const struct settings_ui_state *state); void ui_settings_init(void);
void ui_settings_refresh(const struct settings_ui_state *state, bool animate); void ui_settings_show(struct ui_settings_page *page, bool animate);
void ui_settings_set_visible(bool visible); void ui_settings_deinit(void);
#ifdef __cplusplus #ifdef __cplusplus
} }

60
src/ui/ui_settings_ble.c Normal file
View File

@@ -0,0 +1,60 @@
#include <lvgl.h>
#include <zephyr/sys/util.h>
#include "ui/ui_settings_controller.h"
static uint8_t ble_get_count(struct ui_settings_page *page)
{
ARG_UNUSED(page);
return 4U;
}
static void ble_get_item(struct ui_settings_page *page, uint8_t index,
struct ui_settings_item *item)
{
ARG_UNUSED(page);
if (index < 3U) {
static const char *const titles[] = {
"Slot 1",
"Slot 2",
"Slot 3",
};
item->icon = LV_SYMBOL_BLUETOOTH;
item->title = titles[index];
item->value = ui_settings_ble_slot_label(index);
return;
}
item->icon = LV_SYMBOL_TRASH;
item->title = "Erase Bond";
item->value = ui_settings_ble_current_label();
}
static void ble_on_select(struct ui_settings_page *page, uint8_t index)
{
if (index < 3U) {
ui_settings_ble_select_slot(index);
} else {
ui_settings_ble_erase_current();
}
(void)ui_settings_controller_back();
}
static const struct ui_settings_page_ops ble_ops = {
.get_count = ble_get_count,
.get_item = ble_get_item,
.on_select = ble_on_select,
};
struct ui_settings_page ui_settings_ble_page = {
.base = {
.ops = &ble_ops.base,
},
.ops = &ble_ops,
.title = LV_SYMBOL_BLUETOOTH " Bluetooth",
.hint = "Short: select and return",
};

View File

@@ -0,0 +1,205 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <zephyr/sys/util.h>
#include "ui/ui_settings_controller.h"
#include "settings_view_event.h"
#include "ui_settings_pages.h"
#define BLE_SLOT_COUNT 3U
struct controller_ctx {
struct ui_settings_page *current;
bool active;
uint8_t active_ble_slot;
char ble_labels[BLE_SLOT_COUNT][16];
struct theme_rgb theme;
};
static struct controller_ctx ctx = {
.active_ble_slot = 0U,
.ble_labels = {
"Host A",
"Host B",
"Empty",
},
.theme = {
.r = BLINKY_THEME_DEFAULT_R,
.g = BLINKY_THEME_DEFAULT_G,
.b = BLINKY_THEME_DEFAULT_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 publish_view(bool animate)
{
if (ctx.active && (ctx.current != NULL)) {
submit_settings_view_event(ctx.current, animate);
}
}
void ui_settings_controller_switch_to(struct ui_settings_page *page,
struct ui_page *parent)
{
if (page == NULL) {
return;
}
if (ctx.current != NULL) {
ui_page_deinit(&ctx.current->base);
}
page->base.parent = parent;
ctx.current = page;
ui_page_init(&page->base);
if ((page->ops != NULL) && (page->ops->on_enter != NULL)) {
page->ops->on_enter(page);
}
}
void ui_settings_controller_open(void)
{
ctx.active = true;
ui_settings_controller_switch_to(&ui_settings_root_page, NULL);
publish_view(false);
}
void ui_settings_controller_close(void)
{
if (ctx.current != NULL) {
ui_page_deinit(&ctx.current->base);
ctx.current = NULL;
}
ctx.active = false;
}
bool ui_settings_controller_back(void)
{
struct ui_page *parent;
if (ctx.current == NULL) {
return false;
}
if ((ctx.current->ops != NULL) && (ctx.current->ops->on_back != NULL)) {
ctx.current->ops->on_back(ctx.current);
publish_view(true);
return ctx.active;
}
parent = ctx.current->base.parent;
if (parent == NULL) {
ui_settings_controller_close();
return false;
}
ui_settings_controller_switch_to(ui_page_to_settings(parent),
parent->parent);
publish_view(true);
return true;
}
void ui_settings_controller_select(void)
{
if ((ctx.current == NULL) || (ctx.current->ops == NULL) ||
(ctx.current->ops->on_select == NULL)) {
return;
}
ctx.current->ops->on_select(ctx.current, ctx.current->selected);
publish_view(true);
}
void ui_settings_controller_move(int8_t delta)
{
uint8_t count;
if ((ctx.current == NULL) || (ctx.current->ops == NULL) ||
(ctx.current->ops->get_count == NULL)) {
return;
}
count = ctx.current->ops->get_count(ctx.current);
ctx.current->selected = wrap_index(ctx.current->selected, count, delta);
publish_view(true);
}
void ui_settings_controller_refresh(bool animate)
{
if (ctx.current != NULL) {
publish_view(animate);
}
}
bool ui_settings_controller_is_active(void)
{
return ctx.active;
}
const char *ui_settings_ble_current_label(void)
{
return (ctx.active_ble_slot == 0U) ? "Slot 1" :
(ctx.active_ble_slot == 1U) ? "Slot 2" : "Slot 3";
}
void ui_settings_ble_select_slot(uint8_t slot)
{
if (slot < BLE_SLOT_COUNT) {
ctx.active_ble_slot = slot;
}
}
void ui_settings_ble_erase_current(void)
{
strncpy(ctx.ble_labels[ctx.active_ble_slot], "Empty",
sizeof(ctx.ble_labels[ctx.active_ble_slot]));
ctx.ble_labels[ctx.active_ble_slot]
[sizeof(ctx.ble_labels[ctx.active_ble_slot]) - 1U] = '\0';
}
const char *ui_settings_ble_slot_label(uint8_t slot)
{
if (slot >= BLE_SLOT_COUNT) {
return "";
}
return ctx.ble_labels[slot];
}
void ui_settings_theme_set_current(struct theme_rgb theme)
{
ctx.theme = theme;
}
const char *ui_settings_theme_current_name(void)
{
extern const char *ui_settings_theme_name_for_color(struct theme_rgb theme);
return ui_settings_theme_name_for_color(ctx.theme);
}
uint8_t ui_settings_theme_current_index(void)
{
extern uint8_t ui_settings_theme_index_for_color(struct theme_rgb theme);
return ui_settings_theme_index_for_color(ctx.theme);
}

View File

@@ -0,0 +1,10 @@
#ifndef BLINKY_UI_SETTINGS_PAGES_H_
#define BLINKY_UI_SETTINGS_PAGES_H_
#include "ui/ui_settings_page.h"
extern struct ui_settings_page ui_settings_root_page;
extern struct ui_settings_page ui_settings_ble_page;
extern struct ui_settings_page ui_settings_theme_page;
#endif /* BLINKY_UI_SETTINGS_PAGES_H_ */

64
src/ui/ui_settings_root.c Normal file
View File

@@ -0,0 +1,64 @@
#include <lvgl.h>
#include <zephyr/sys/util.h>
#include "ui/ui_settings_controller.h"
#include "ui_settings_pages.h"
static uint8_t root_get_count(struct ui_settings_page *page)
{
ARG_UNUSED(page);
return 2U;
}
static void root_get_item(struct ui_settings_page *page, uint8_t index,
struct ui_settings_item *item)
{
ARG_UNUSED(page);
if (index == 0U) {
item->icon = LV_SYMBOL_BLUETOOTH;
item->title = "Bluetooth";
item->value = ui_settings_ble_current_label();
return;
}
item->icon = LV_SYMBOL_TINT;
item->title = "Theme";
item->value = ui_settings_theme_current_name();
}
static void root_on_enter(struct ui_settings_page *page)
{
page->selected = 0U;
}
static void root_on_select(struct ui_settings_page *page, uint8_t index)
{
ui_settings_controller_switch_to(
(index == 0U) ? &ui_settings_ble_page : &ui_settings_theme_page,
&page->base);
}
static void root_on_back(struct ui_settings_page *page)
{
ARG_UNUSED(page);
ui_settings_controller_close();
}
static const struct ui_settings_page_ops root_ops = {
.get_count = root_get_count,
.get_item = root_get_item,
.on_enter = root_on_enter,
.on_select = root_on_select,
.on_back = root_on_back,
};
struct ui_settings_page ui_settings_root_page = {
.base = {
.ops = &root_ops.base,
},
.ops = &root_ops,
.title = LV_SYMBOL_SETTINGS " Settings",
.hint = "Rotate select Tap OK Hold exit",
};

109
src/ui/ui_settings_theme.c Normal file
View File

@@ -0,0 +1,109 @@
#include <lvgl.h>
#include <zephyr/sys/util.h>
#include "theme_rgb_update_event.h"
#include "ui/ui_settings_controller.h"
struct theme_option {
const char *name;
struct theme_rgb color;
};
static const struct theme_option themes[] = {
{ "Red", { .r = 0xFF, .g = 0x00, .b = 0x00 } },
{ "Amber", { .r = 0xFF, .g = 0x95, .b = 0x00 } },
{ "Default", { .r = BLINKY_THEME_DEFAULT_R,
.g = BLINKY_THEME_DEFAULT_G,
.b = BLINKY_THEME_DEFAULT_B } },
{ "Green", { .r = 0x34, .g = 0xC7, .b = 0x59 } },
{ "Purple", { .r = 0xBF, .g = 0x5A, .b = 0xF2 } },
{ "White", { .r = 0xF2, .g = 0xF2, .b = 0xF7 } },
};
static bool theme_equal(struct theme_rgb lhs, struct theme_rgb rhs)
{
return (lhs.r == rhs.r) && (lhs.g == rhs.g) && (lhs.b == rhs.b);
}
uint8_t ui_settings_theme_index_for_color(struct theme_rgb theme)
{
for (uint8_t i = 0; i < ARRAY_SIZE(themes); i++) {
if (theme_equal(theme, themes[i].color)) {
return i;
}
}
return 0U;
}
const char *ui_settings_theme_name_for_color(struct theme_rgb theme)
{
for (uint8_t i = 0; i < ARRAY_SIZE(themes); i++) {
if (theme_equal(theme, themes[i].color)) {
return themes[i].name;
}
}
return "Custom";
}
static void draw_swatch(const struct ui_settings_item *item, lv_obj_t *container)
{
const struct theme_rgb *color = item->user_data;
if (color == NULL) {
return;
}
lv_obj_set_style_bg_opa(container, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(container,
lv_color_make(color->r, color->g, color->b), 0);
}
static uint8_t theme_get_count(struct ui_settings_page *page)
{
ARG_UNUSED(page);
return ARRAY_SIZE(themes);
}
static void theme_get_item(struct ui_settings_page *page, uint8_t index,
struct ui_settings_item *item)
{
ARG_UNUSED(page);
item->icon = LV_SYMBOL_TINT;
item->title = themes[index].name;
item->draw = draw_swatch;
item->user_data = (void *)&themes[index].color;
}
static void theme_on_enter(struct ui_settings_page *page)
{
page->selected = ui_settings_theme_current_index();
}
static void theme_on_select(struct ui_settings_page *page, uint8_t index)
{
struct theme_rgb theme = themes[index].color;
ui_settings_theme_set_current(theme);
submit_theme_rgb_update_event(theme);
(void)ui_settings_controller_back();
}
static const struct ui_settings_page_ops theme_ops = {
.get_count = theme_get_count,
.get_item = theme_get_item,
.on_enter = theme_on_enter,
.on_select = theme_on_select,
};
struct ui_settings_page ui_settings_theme_page = {
.base = {
.ops = &theme_ops.base,
},
.ops = &theme_ops,
.title = LV_SYMBOL_TINT " Theme",
.hint = "Short: apply and return",
};