diff --git a/CMakeLists.txt b/CMakeLists.txt index 277f404..59e035a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,10 @@ target_sources(app PRIVATE src/settings_module.c src/ui/ui_main.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/usb_cdc_module.c src/usb_device_module.c diff --git a/inc/events/settings_view_event.h b/inc/events/settings_view_event.h index 42d744f..548e879 100644 --- a/inc/events/settings_view_event.h +++ b/inc/events/settings_view_event.h @@ -1,12 +1,10 @@ #ifndef BLINKY_SETTINGS_VIEW_EVENT_H_ #define BLINKY_SETTINGS_VIEW_EVENT_H_ -#include - #include #include -#include "settings_ui.h" +#include "ui/ui_settings_page.h" #ifdef __cplusplus extern "C" { @@ -14,22 +12,19 @@ extern "C" { struct settings_view_event { struct app_event_header header; - struct settings_ui_state state; + struct ui_settings_page *page; + bool animate; }; APP_EVENT_TYPE_DECLARE(settings_view_event); -static inline void submit_settings_view_event( - const struct settings_ui_state *state) +static inline void submit_settings_view_event(struct ui_settings_page *page, + bool animate) { - struct settings_view_event *event; + struct settings_view_event *event = new_settings_view_event(); - if (state == NULL) { - return; - } - - event = new_settings_view_event(); - memcpy(&event->state, state, sizeof(event->state)); + event->page = page; + event->animate = animate; APP_EVENT_SUBMIT(event); } diff --git a/inc/settings_ui.h b/inc/settings_ui.h deleted file mode 100644 index a4a354f..0000000 --- a/inc/settings_ui.h +++ /dev/null @@ -1,56 +0,0 @@ -#ifndef BLINKY_SETTINGS_UI_H_ -#define BLINKY_SETTINGS_UI_H_ - -#include -#include - -#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_ */ diff --git a/inc/ui/ui_page.h b/inc/ui/ui_page.h new file mode 100644 index 0000000..6c2199f --- /dev/null +++ b/inc/ui/ui_page.h @@ -0,0 +1,66 @@ +#ifndef BLINKY_UI_PAGE_H_ +#define BLINKY_UI_PAGE_H_ + +#include + +#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_ */ diff --git a/inc/ui/ui_settings_controller.h b/inc/ui/ui_settings_controller.h new file mode 100644 index 0000000..d9fac87 --- /dev/null +++ b/inc/ui/ui_settings_controller.h @@ -0,0 +1,38 @@ +#ifndef BLINKY_UI_SETTINGS_CONTROLLER_H_ +#define BLINKY_UI_SETTINGS_CONTROLLER_H_ + +#include +#include + +#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_ */ diff --git a/inc/ui/ui_settings_page.h b/inc/ui/ui_settings_page.h new file mode 100644 index 0000000..d39c875 --- /dev/null +++ b/inc/ui/ui_settings_page.h @@ -0,0 +1,56 @@ +#ifndef BLINKY_UI_SETTINGS_PAGE_H_ +#define BLINKY_UI_SETTINGS_PAGE_H_ + +#include + +#include + +#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_ */ diff --git a/src/display_module.c b/src/display_module.c index 550701c..12dbdb5 100644 --- a/src/display_module.c +++ b/src/display_module.c @@ -19,10 +19,10 @@ #include "module_lifecycle.h" #include "mode_switch_event.h" #include "settings_mode_event.h" -#include "settings_ui.h" #include "settings_view_event.h" #include "theme_rgb_update_event.h" #include "theme_color.h" +#include "ui/ui_page.h" #include "ui/ui_main.h" #include "ui/ui_settings.h" @@ -38,9 +38,8 @@ struct display_module_ctx { const struct device *backlight_dev; uint32_t backlight_idx; struct ui_main_model ui_model; - struct settings_ui_state settings_ui; + struct ui_settings_page *settings_page; bool settings_active; - bool settings_ui_valid; bool lvgl_initialized; char date_text[DATETIME_EVENT_DATE_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", }; +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) { if (on) { @@ -133,11 +137,16 @@ static int do_start(void) ctx.lvgl_initialized = true; lvgl_lock(); - ui_main_init(&ctx.ui_model, ctx.date_text, ctx.time_text); - ui_settings_init(NULL); + ui_page_init(main_page()); lvgl_unlock(); } + lvgl_lock(); + if (!ctx.settings_active) { + ui_page_init(main_page()); + } + lvgl_unlock(); + err = backlight_set(true); if (err) { LOG_ERR("Backlight enable failed (%d)", err); @@ -175,15 +184,11 @@ static void refresh_ui(void) return; } - if (ctx.settings_active && !ctx.settings_ui_valid) { - return; - } - lvgl_lock(); - if (ctx.settings_active) { - ui_settings_refresh(&ctx.settings_ui, true); + if (ctx.settings_active && (ctx.settings_page != NULL)) { + ui_settings_show(ctx.settings_page, true); } else { - ui_main_refresh_all(&ctx.ui_model, ctx.date_text, ctx.time_text); + ui_page_refresh(main_page()); } 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, event->theme.g, event->theme.b); - ctx.settings_ui.accent = event->theme; refresh_ui(); return false; } @@ -233,17 +237,24 @@ static bool app_event_handler(const struct app_event_header *aeh) cast_settings_mode_event(aeh); ctx.settings_active = event->active; - ctx.settings_ui_valid = false; + if (!ctx.settings_active) { + ctx.settings_page = NULL; + } if (!ctx.lvgl_initialized) { return false; } lvgl_lock(); - if (!ctx.settings_active) { - ui_settings_set_visible(false); - ui_main_set_visible(true); - ui_main_refresh_all(&ctx.ui_model, ctx.date_text, - ctx.time_text); + if (ctx.settings_active) { + ui_page_deinit(main_page()); + ui_settings_init(); + if (ctx.settings_page != NULL) { + ui_settings_show(ctx.settings_page, false); + } + } else { + ui_settings_deinit(); + ui_page_init(main_page()); + ui_page_refresh(main_page()); } lvgl_unlock(); return false; @@ -253,18 +264,13 @@ static bool app_event_handler(const struct app_event_header *aeh) const struct settings_view_event *event = cast_settings_view_event(aeh); - ctx.settings_ui = event->state; - ctx.settings_ui_valid = true; + ctx.settings_page = event->page; if (ctx.settings_active && ctx.lvgl_initialized) { lvgl_lock(); - ui_settings_refresh(&ctx.settings_ui, false); - ui_main_set_visible(false); - ui_settings_set_visible(true); + ui_settings_show(ctx.settings_page, event->animate); lvgl_unlock(); - return false; } - refresh_ui(); return false; } diff --git a/src/events/settings_view_event.c b/src/events/settings_view_event.c index 4e56d05..518ca2b 100644 --- a/src/events/settings_view_event.c +++ b/src/events/settings_view_event.c @@ -1,30 +1,14 @@ -#include "settings_view_event.h" +#include -static const char *page_name(enum settings_ui_page page) -{ - 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 "?"; - } -} +#include "settings_view_event.h" static void log_settings_view_event(const struct app_event_header *aeh) { const struct settings_view_event *event = cast_settings_view_event(aeh); - APP_EVENT_MANAGER_LOG( - aeh, - "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); + APP_EVENT_MANAGER_LOG(aeh, "page:%p animate:%u", + event->page, event->animate); } 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 = cast_settings_view_event(aeh); - nrf_profiler_log_encode_uint8(buf, event->state.active ? 1U : 0U); - nrf_profiler_log_encode_uint8(buf, event->state.page); - 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); + nrf_profiler_log_encode_uint32(buf, (uint32_t)(uintptr_t)event->page); + nrf_profiler_log_encode_uint8(buf, event->animate ? 1U : 0U); } APP_EVENT_INFO_DEFINE(settings_view_event, - ENCODE(NRF_PROFILER_ARG_U8, - NRF_PROFILER_ARG_U8, - 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"), + ENCODE(NRF_PROFILER_ARG_U32, NRF_PROFILER_ARG_U8), + ENCODE("page", "animate"), profile_settings_view_event); APP_EVENT_TYPE_DEFINE(settings_view_event, diff --git a/src/settings_module.c b/src/settings_module.c index 54dbd34..22c8273 100644 --- a/src/settings_module.c +++ b/src/settings_module.c @@ -10,53 +10,22 @@ #include #include -#include #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; diff --git a/src/ui/ui_main.c b/src/ui/ui_main.c index 1993e26..67e4702 100644 --- a/src/ui/ui_main.c +++ b/src/ui/ui_main.c @@ -31,6 +31,9 @@ struct ui_main_ctx { static struct ui_main_ctx g_ui; 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] = { LV_SYMBOL_USB, @@ -297,15 +300,55 @@ void ui_main_init(const struct ui_main_model *model, 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; } - if (visible) { - lv_obj_clear_flag(g_ui.content, LV_OBJ_FLAG_HIDDEN); - } 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)); + 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; } diff --git a/src/ui/ui_main.h b/src/ui/ui_main.h index 0443df8..d75b6fe 100644 --- a/src/ui/ui_main.h +++ b/src/ui/ui_main.h @@ -7,6 +7,7 @@ #include #include "mode_switch_event.h" +#include "ui/ui_page.h" #ifdef __cplusplus 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_battery(const struct ui_main_model *model); 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 } diff --git a/src/ui/ui_settings.c b/src/ui/ui_settings.c index bd330f9..c3f1619 100644 --- a/src/ui/ui_settings.c +++ b/src/ui/ui_settings.c @@ -1,4 +1,3 @@ -#include #include #include @@ -6,67 +5,41 @@ #include "ui_settings.h" #define UI_SETTINGS_LIST_H 104 -#define UI_SETTINGS_ROOT_MAX SETTINGS_UI_ROOT_ITEM_COUNT -#define UI_SETTINGS_BLE_MAX SETTINGS_UI_BLE_ITEM_COUNT -#define UI_SETTINGS_THEME_MAX SETTINGS_UI_THEME_OPTION_COUNT +#define UI_SETTINGS_MAX_ITEMS 8U struct ui_settings_row { lv_obj_t *row; lv_obj_t *icon; - lv_obj_t *swatch; lv_obj_t *title; lv_obj_t *value; + lv_obj_t *custom; }; struct ui_settings_ctx { lv_obj_t *content; lv_obj_t *title; lv_obj_t *hint; - lv_obj_t *root_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 *scroll; lv_obj_t *value; + struct ui_settings_row rows[UI_SETTINGS_MAX_ITEMS]; lv_group_t *focus_group; + uint8_t row_count; + struct ui_settings_page *page; bool initialized; }; 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) { - return; - } - - if (visible) { - lv_obj_clear_flag(scroll, LV_OBJ_FLAG_HIDDEN); - } else { - lv_obj_add_flag(scroll, LV_OBJ_FLAG_HIDDEN); - } + lv_obj_clean(row->custom); + 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); } -static void create_scroll(lv_obj_t *parent, lv_obj_t **scroll) +static void create_row(struct ui_settings_row *row) { - *scroll = lv_obj_create(parent); - 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); + row->row = lv_obj_create(g_ui.scroll); lv_obj_remove_style_all(row->row); lv_obj_set_width(row->row, LV_PCT(100)); 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_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); lv_obj_set_width(row->title, 118); 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_align(row->value, LV_TEXT_ALIGN_RIGHT, 0); 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, - const char *title, const char *value, bool selected, - lv_color_t accent, const struct theme_rgb *swatch) +static void rebuild_rows(uint8_t count) { - lv_label_set_text(row->icon, icon ? icon : ""); - lv_label_set_text(row->title, title ? title : ""); - lv_label_set_text(row->value, value ? value : ""); + if (count > UI_SETTINGS_MAX_ITEMS) { + count = UI_SETTINGS_MAX_ITEMS; + } + 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, selected ? accent : lv_color_hex(0x0B1017), 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) { - lv_obj_set_style_bg_color(row->swatch, - lv_color_make(swatch->r, swatch->g, - swatch->b), - 0); - } else { - lv_obj_set_style_bg_color(row->swatch, lv_color_hex(0x0B1017), 0); + row_clear_custom(row); + if (item->draw != NULL) { + item->draw(item, row->custom); } } -static void focus_row(lv_obj_t *scroll, struct ui_settings_row *row, - bool animate) +static void focus_row(uint8_t index, bool animate) { - lv_obj_update_layout(scroll); - lv_group_focus_obj(row->row); - 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_color_t accent, bool animate) -{ - for (uint8_t i = 0; i < UI_SETTINGS_ROOT_MAX; i++) { - 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); + if (index >= g_ui.row_count) { + return; } - focus_row(g_ui.root_scroll, &g_ui.root_rows[state->root_selected], animate); + lv_obj_update_layout(g_ui.scroll); + lv_group_focus_obj(g_ui.rows[index].row); + lv_obj_scroll_to_view(g_ui.rows[index].row, + animate ? LV_ANIM_ON : LV_ANIM_OFF); } -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) +void ui_settings_init(void) { lv_obj_t *screen = lv_screen_active(); if (g_ui.initialized) { - ui_settings_refresh(state, false); return; } @@ -231,22 +164,19 @@ void ui_settings_init(const struct settings_ui_state *state) 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_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); - for (uint8_t i = 0; i < UI_SETTINGS_ROOT_MAX; i++) { - create_row(g_ui.root_scroll, &g_ui.root_rows[i]); - } - - create_scroll(g_ui.content, &g_ui.ble_scroll); - for (uint8_t i = 0; i < UI_SETTINGS_BLE_MAX; i++) { - create_row(g_ui.ble_scroll, &g_ui.ble_rows[i]); - } - - create_scroll(g_ui.content, &g_ui.theme_scroll); - for (uint8_t i = 0; i < UI_SETTINGS_THEME_MAX; i++) { - create_row(g_ui.theme_scroll, &g_ui.theme_rows[i]); - } + g_ui.scroll = lv_obj_create(g_ui.content); + lv_obj_remove_style_all(g_ui.scroll); + 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); + lv_obj_set_layout(g_ui.scroll, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(g_ui.scroll, LV_FLEX_FLOW_COLUMN); + 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); + lv_obj_set_scroll_dir(g_ui.scroll, LV_DIR_VER); + lv_obj_set_scrollbar_mode(g_ui.scroll, LV_SCROLLBAR_MODE_OFF); g_ui.value = lv_label_create(g_ui.content); 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); 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; } - 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); - scroll_set_visible(g_ui.ble_scroll, false); - scroll_set_visible(g_ui.theme_scroll, false); + lv_label_set_text(g_ui.title, page->title ? page->title : ""); + lv_label_set_text(g_ui.hint, page->hint ? page->hint : ""); + lv_label_set_text(g_ui.value, ""); - switch (state->page) { - case SETTINGS_UI_PAGE_ROOT: - 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; + for (uint8_t i = 0; i < g_ui.row_count; i++) { + struct ui_settings_item item = { 0 }; - case SETTINGS_UI_PAGE_BLE: - scroll_set_visible(g_ui.ble_scroll, true); - 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; + page->ops->get_item(page, i, &item); + row_set(&g_ui.rows[i], &item, page->selected == i, accent); } + + focus_row(page->selected, animate); } -void ui_settings_set_visible(bool visible) +void ui_settings_deinit(void) { - if (!g_ui.initialized || (g_ui.content == NULL)) { + if (!g_ui.initialized) { return; } - if (visible) { - lv_obj_clear_flag(g_ui.content, LV_OBJ_FLAG_HIDDEN); - } else { - lv_obj_add_flag(g_ui.content, LV_OBJ_FLAG_HIDDEN); + if (g_ui.focus_group != NULL) { + lv_group_delete(g_ui.focus_group); } + + if (g_ui.content != NULL) { + lv_obj_delete(g_ui.content); + } + + memset(&g_ui, 0, sizeof(g_ui)); } diff --git a/src/ui/ui_settings.h b/src/ui/ui_settings.h index 0891f77..64ed1c0 100644 --- a/src/ui/ui_settings.h +++ b/src/ui/ui_settings.h @@ -1,15 +1,15 @@ #ifndef BLINKY_UI_SETTINGS_H_ #define BLINKY_UI_SETTINGS_H_ -#include "settings_ui.h" +#include "ui/ui_settings_page.h" #ifdef __cplusplus extern "C" { #endif -void ui_settings_init(const struct settings_ui_state *state); -void ui_settings_refresh(const struct settings_ui_state *state, bool animate); -void ui_settings_set_visible(bool visible); +void ui_settings_init(void); +void ui_settings_show(struct ui_settings_page *page, bool animate); +void ui_settings_deinit(void); #ifdef __cplusplus } diff --git a/src/ui/ui_settings_ble.c b/src/ui/ui_settings_ble.c new file mode 100644 index 0000000..2a1945c --- /dev/null +++ b/src/ui/ui_settings_ble.c @@ -0,0 +1,60 @@ +#include + +#include + +#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", +}; diff --git a/src/ui/ui_settings_controller.c b/src/ui/ui_settings_controller.c new file mode 100644 index 0000000..0c00c17 --- /dev/null +++ b/src/ui/ui_settings_controller.c @@ -0,0 +1,205 @@ +#include +#include +#include +#include + +#include + +#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); +} diff --git a/src/ui/ui_settings_pages.h b/src/ui/ui_settings_pages.h new file mode 100644 index 0000000..0b9f18a --- /dev/null +++ b/src/ui/ui_settings_pages.h @@ -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_ */ diff --git a/src/ui/ui_settings_root.c b/src/ui/ui_settings_root.c new file mode 100644 index 0000000..3257000 --- /dev/null +++ b/src/ui/ui_settings_root.c @@ -0,0 +1,64 @@ +#include + +#include + +#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", +}; diff --git a/src/ui/ui_settings_theme.c b/src/ui/ui_settings_theme.c new file mode 100644 index 0000000..f47b1c5 --- /dev/null +++ b/src/ui/ui_settings_theme.c @@ -0,0 +1,109 @@ +#include + +#include + +#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", +};