diff --git a/CMakeLists.txt b/CMakeLists.txt index 6369b5c..277f404 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,7 +35,9 @@ target_sources(app PRIVATE src/led_strip_module.c src/mode_policy_module.c src/time_sync_module.c + src/settings_module.c src/ui/ui_main.c + src/ui/ui_settings.c src/protocol_module.c src/usb_cdc_module.c src/usb_device_module.c @@ -58,6 +60,8 @@ target_sources(app PRIVATE src/events/proto_transport_state_event.c src/events/proto_tx_event.c src/events/set_protocol_event.c + src/events/settings_mode_event.c + src/events/settings_view_event.c src/events/theme_rgb_update_event.c src/events/time_sync_event.c src/events/transport_policy_event.c diff --git a/inc/events/settings_mode_event.h b/inc/events/settings_mode_event.h new file mode 100644 index 0000000..b79f1c6 --- /dev/null +++ b/inc/events/settings_mode_event.h @@ -0,0 +1,30 @@ +#ifndef BLINKY_SETTINGS_MODE_EVENT_H_ +#define BLINKY_SETTINGS_MODE_EVENT_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct settings_mode_event { + struct app_event_header header; + bool active; +}; + +APP_EVENT_TYPE_DECLARE(settings_mode_event); + +static inline void submit_settings_mode_event(bool active) +{ + struct settings_mode_event *event = new_settings_mode_event(); + + event->active = active; + APP_EVENT_SUBMIT(event); +} + +#ifdef __cplusplus +} +#endif + +#endif /* BLINKY_SETTINGS_MODE_EVENT_H_ */ diff --git a/inc/events/settings_view_event.h b/inc/events/settings_view_event.h new file mode 100644 index 0000000..42d744f --- /dev/null +++ b/inc/events/settings_view_event.h @@ -0,0 +1,40 @@ +#ifndef BLINKY_SETTINGS_VIEW_EVENT_H_ +#define BLINKY_SETTINGS_VIEW_EVENT_H_ + +#include + +#include +#include + +#include "settings_ui.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct settings_view_event { + struct app_event_header header; + struct settings_ui_state state; +}; + +APP_EVENT_TYPE_DECLARE(settings_view_event); + +static inline void submit_settings_view_event( + const struct settings_ui_state *state) +{ + struct settings_view_event *event; + + if (state == NULL) { + return; + } + + event = new_settings_view_event(); + memcpy(&event->state, state, sizeof(event->state)); + APP_EVENT_SUBMIT(event); +} + +#ifdef __cplusplus +} +#endif + +#endif /* BLINKY_SETTINGS_VIEW_EVENT_H_ */ diff --git a/inc/settings_ui.h b/inc/settings_ui.h new file mode 100644 index 0000000..a4a354f --- /dev/null +++ b/inc/settings_ui.h @@ -0,0 +1,56 @@ +#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/prj.conf b/prj.conf index 0442222..e0acd89 100644 --- a/prj.conf +++ b/prj.conf @@ -3,6 +3,7 @@ CONFIG_CAF_BUTTONS=y CONFIG_CAF_BUTTONS_DEF_PATH="buttons_def.h" CONFIG_CAF_CLICK_DETECTOR=y CONFIG_CAF_CLICK_DETECTOR_DEF_PATH="click_detector_def.h" +CONFIG_CAF_CLICK_DETECTOR_LONG_CLICK_MSEC=1500 CONFIG_GPIO=y CONFIG_I2C=y CONFIG_LED=y @@ -110,10 +111,6 @@ CONFIG_CAF_BLE_ADV_FAST_ADV=y CONFIG_CAF_BLE_ADV_FILTER_ACCEPT_LIST=y CONFIG_CAF_BLE_ADV_MODULE_SUSPEND_EVENTS=y CONFIG_CAF_BLE_BOND=y -CONFIG_CAF_BLE_BOND_PEER_ERASE_CLICK=y -CONFIG_CAF_BLE_BOND_PEER_ERASE_CLICK_KEY_ID=0x180 -CONFIG_CAF_BLE_BOND_PEER_ERASE_CLICK_LONG=y -CONFIG_CAF_BLE_BOND_PEER_ERASE_CLICK_TIMEOUT=-1 CONFIG_CAF_MODULE_SUSPEND_EVENTS=y CONFIG_BT_ADV_PROV_FLAGS=y CONFIG_BT_ADV_PROV_GAP_APPEARANCE=y @@ -131,7 +128,7 @@ CONFIG_LV_COLOR_16_SWAP=y CONFIG_LV_Z_BITS_PER_PIXEL=16 CONFIG_LV_Z_VDB_SIZE=25 CONFIG_LV_Z_DOUBLE_VDB=y -CONFIG_LV_Z_MEM_POOL_SIZE=16384 +CONFIG_LV_Z_MEM_POOL_SIZE=32768 CONFIG_LV_USE_LABEL=y CONFIG_LV_FONT_MONTSERRAT_14=y CONFIG_LV_FONT_MONTSERRAT_32=y diff --git a/src/display_module.c b/src/display_module.c index bc9ebca..550701c 100644 --- a/src/display_module.c +++ b/src/display_module.c @@ -18,9 +18,13 @@ #include "hid_led_event.h" #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_main.h" +#include "ui/ui_settings.h" LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF); @@ -34,6 +38,9 @@ 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; + 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]; @@ -127,6 +134,7 @@ static int do_start(void) lvgl_lock(); ui_main_init(&ctx.ui_model, ctx.date_text, ctx.time_text); + ui_settings_init(NULL); lvgl_unlock(); } @@ -167,8 +175,16 @@ static void refresh_ui(void) return; } + if (ctx.settings_active && !ctx.settings_ui_valid) { + return; + } + lvgl_lock(); - ui_main_refresh_all(&ctx.ui_model, ctx.date_text, ctx.time_text); + if (ctx.settings_active) { + ui_settings_refresh(&ctx.settings_ui, true); + } else { + ui_main_refresh_all(&ctx.ui_model, ctx.date_text, ctx.time_text); + } lvgl_unlock(); } @@ -207,6 +223,47 @@ 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; + } + + if (is_settings_mode_event(aeh)) { + const struct settings_mode_event *event = + cast_settings_mode_event(aeh); + + ctx.settings_active = event->active; + ctx.settings_ui_valid = false; + 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); + } + lvgl_unlock(); + return false; + } + + if (is_settings_view_event(aeh)) { + const struct settings_view_event *event = + cast_settings_view_event(aeh); + + ctx.settings_ui = event->state; + ctx.settings_ui_valid = true; + 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); + lvgl_unlock(); + return false; + } + refresh_ui(); return false; } @@ -257,6 +314,8 @@ APP_EVENT_SUBSCRIBE(MODULE, datetime_event); APP_EVENT_SUBSCRIBE(MODULE, hid_led_event); APP_EVENT_SUBSCRIBE(MODULE, module_state_event); APP_EVENT_SUBSCRIBE(MODULE, mode_switch_event); +APP_EVENT_SUBSCRIBE(MODULE, settings_mode_event); +APP_EVENT_SUBSCRIBE(MODULE, settings_view_event); APP_EVENT_SUBSCRIBE(MODULE, theme_rgb_update_event); APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event); APP_EVENT_SUBSCRIBE(MODULE, wake_up_event); diff --git a/src/events/settings_mode_event.c b/src/events/settings_mode_event.c new file mode 100644 index 0000000..30b8606 --- /dev/null +++ b/src/events/settings_mode_event.c @@ -0,0 +1,29 @@ +#include "settings_mode_event.h" + +static void log_settings_mode_event(const struct app_event_header *aeh) +{ + const struct settings_mode_event *event = + cast_settings_mode_event(aeh); + + APP_EVENT_MANAGER_LOG(aeh, "active:%u", event->active); +} + +static void profile_settings_mode_event(struct log_event_buf *buf, + const struct app_event_header *aeh) +{ + const struct settings_mode_event *event = + cast_settings_mode_event(aeh); + + nrf_profiler_log_encode_uint8(buf, event->active ? 1U : 0U); +} + +APP_EVENT_INFO_DEFINE(settings_mode_event, + ENCODE(NRF_PROFILER_ARG_U8), + ENCODE("active"), + profile_settings_mode_event); + +APP_EVENT_TYPE_DEFINE(settings_mode_event, + log_settings_mode_event, + &settings_mode_event_info, + APP_EVENT_FLAGS_CREATE( + APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE)); diff --git a/src/events/settings_view_event.c b/src/events/settings_view_event.c new file mode 100644 index 0000000..4e56d05 --- /dev/null +++ b/src/events/settings_view_event.c @@ -0,0 +1,59 @@ +#include "settings_view_event.h" + +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 "?"; + } +} + +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); +} + +static void profile_settings_view_event(struct log_event_buf *buf, + const struct app_event_header *aeh) +{ + 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); +} + +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"), + profile_settings_view_event); + +APP_EVENT_TYPE_DEFINE(settings_view_event, + log_settings_view_event, + &settings_view_event_info, + APP_EVENT_FLAGS_CREATE( + APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE)); diff --git a/src/keyboard_core_module.c b/src/keyboard_core_module.c index 14f1835..b5fff6c 100644 --- a/src/keyboard_core_module.c +++ b/src/keyboard_core_module.c @@ -20,6 +20,7 @@ #include "keyboard_core.h" #include "keyboard_hid_report_event.h" #include "module_lifecycle.h" +#include "settings_mode_event.h" #include "set_protocol_event.h" #include "transport_policy_event.h" @@ -94,6 +95,7 @@ struct keyboard_core_module_ctx { enum keyboard_protocol_mode transport_protocol_modes[HID_TRANSPORT_COUNT]; enum hid_transport_policy current_transport; bool mode_valid; + bool settings_active; }; static int do_init(void); @@ -130,6 +132,7 @@ static struct keyboard_core_module_ctx ctx = { #define transport_protocol_modes ctx.transport_protocol_modes #define current_transport ctx.current_transport #define mode_valid ctx.mode_valid +#define settings_active ctx.settings_active #define running module_lifecycle_is_running(&ctx.lc) static bool policy_to_transport(enum hid_transport_policy policy, @@ -364,6 +367,7 @@ static void submit_consumer_fifo_frame(uint16_t usage_id) enum mode_switch_mode mode; if (!running || !mode_valid || + settings_active || !transport_policy_to_mode(current_transport, &mode) || (protocol_mode == KEYBOARD_PROTOCOL_MODE_BOOT)) { return; @@ -414,6 +418,10 @@ static void emit_keys_report(bool force) return; } + if (settings_active) { + return; + } + if (protocol_mode == KEYBOARD_PROTOCOL_MODE_BOOT) { build_boot_report(report_buf); report_size = KEYBOARD_BOOT_REPORT_SIZE; @@ -445,6 +453,7 @@ static void emit_consumer_report(bool force) enum mode_switch_mode mode; if (!mode_valid || !transport_policy_to_mode(current_transport, &mode) || + settings_active || (protocol_mode == KEYBOARD_PROTOCOL_MODE_BOOT)) { return; } @@ -518,6 +527,7 @@ static int do_init(void) reports_cache_invalidate(); function_usage_mask_clear(); mode_valid = false; + settings_active = false; transport_protocol_modes[HID_TRANSPORT_USB] = KEYBOARD_PROTOCOL_MODE_REPORT; transport_protocol_modes[HID_TRANSPORT_BLE] = @@ -562,6 +572,10 @@ static bool handle_button_event(const struct button_event *event) return false; } + if (settings_active) { + return false; + } + entry = keymap_get(event->key_id); if (!entry) { LOG_WRN("Unmapped key id 0x%04x", event->key_id); @@ -639,6 +653,10 @@ static bool handle_encoder_event(const struct encoder_event *event) return false; } + if (settings_active) { + return false; + } + if (event->detents > 0) { submit_consumer_pulse_frames(KEYBOARD_CONSUMER_CTRL_VOLUME_UP, (uint8_t)event->detents); @@ -703,6 +721,27 @@ static bool app_event_handler(const struct app_event_header *aeh) cast_transport_policy_event(aeh)); } + if (is_settings_mode_event(aeh)) { + const struct settings_mode_event *event = + cast_settings_mode_event(aeh); + + if (settings_active == event->active) { + return false; + } + + settings_active = event->active; + if (settings_active) { + if (mode_valid) { + emit_release_reports(current_transport); + } + emit_function_state_event(); + keyboard_state_clear(); + reports_cache_invalidate(); + } + + return false; + } + if (is_module_state_event(aeh)) { const struct module_state_event *event = cast_module_state_event(aeh); @@ -738,6 +777,7 @@ APP_EVENT_SUBSCRIBE(MODULE, button_event); APP_EVENT_SUBSCRIBE(MODULE, encoder_event); APP_EVENT_SUBSCRIBE(MODULE, function_bitmap_update_event); APP_EVENT_SUBSCRIBE(MODULE, set_protocol_event); +APP_EVENT_SUBSCRIBE(MODULE, settings_mode_event); APP_EVENT_SUBSCRIBE(MODULE, transport_policy_event); APP_EVENT_SUBSCRIBE(MODULE, module_state_event); APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event); diff --git a/src/settings_module.c b/src/settings_module.c new file mode 100644 index 0000000..54dbd34 --- /dev/null +++ b/src/settings_module.c @@ -0,0 +1,502 @@ +#include +#include +#include + +#include + +#define MODULE settings_module +#include +#include +#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" + +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); +static int do_start(void); +static int do_stop(void); + +static const struct module_lifecycle_cfg lifecycle_cfg = { + .mode = ML_MODE_POWER, + .stopped_state = MODULE_STATE_STANDBY, +}; + +static const struct module_lifecycle_ops lifecycle_ops = { + .do_init = do_init, + .do_start = do_start, + .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, + .cfg = &lifecycle_cfg, + .ops = &lifecycle_ops, + }, +}; + +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) { + return; + } + + ctx.active = false; + ctx.page = SETTINGS_UI_PAGE_ROOT; + submit_settings_mode_event(false); + publish_view_state(); +} + +static void settings_enter(void) +{ + if (ctx.active) { + return; + } + + 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; + } +} + +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"); + + return 0; +} + +static int do_start(void) +{ + return 0; +} + +static int do_stop(void) +{ + settings_exit(); + return 0; +} + +static bool handle_click_event(const struct click_event *event) +{ + if (!module_lifecycle_is_running(&ctx.lc) || + (event->key_id != SETTINGS_MUTE_KEY_ID)) { + return false; + } + + if (!ctx.active) { + if (event->click == CLICK_LONG) { + settings_enter(); + } + + return false; + } + + switch (event->click) { + case CLICK_SHORT: + handle_confirm(); + break; + + case CLICK_LONG: + handle_back(); + break; + + default: + break; + } + + return false; +} + +static bool handle_encoder_event(const struct encoder_event *event) +{ + if (!module_lifecycle_is_running(&ctx.lc) || !ctx.active) { + return false; + } + + handle_navigation(event->detents); + return false; +} + +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; + } + + if (ctx.active) { + publish_view_state(); + } + + return false; +} + +static bool app_event_handler(const struct app_event_header *aeh) +{ + if (is_click_event(aeh)) { + return handle_click_event(cast_click_event(aeh)); + } + + if (is_encoder_event(aeh)) { + return handle_encoder_event(cast_encoder_event(aeh)); + } + + if (is_theme_rgb_update_event(aeh)) { + return handle_theme_rgb_update_event( + cast_theme_rgb_update_event(aeh)); + } + + if (is_module_state_event(aeh)) { + const struct module_state_event *event = cast_module_state_event(aeh); + + if (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) { + (void)module_set_lifecycle(&ctx.lc, LC_RUNNING); + } + + return false; + } + + if (is_power_down_event(aeh)) { + if (module_lifecycle_is_initialized(&ctx.lc)) { + (void)module_set_lifecycle(&ctx.lc, LC_STOPPED); + } + + return false; + } + + if (is_wake_up_event(aeh)) { + if (module_lifecycle_is_initialized(&ctx.lc)) { + (void)module_set_lifecycle(&ctx.lc, LC_RUNNING); + } + + return false; + } + + return false; +} + +APP_EVENT_LISTENER(MODULE, app_event_handler); +APP_EVENT_SUBSCRIBE(MODULE, click_event); +APP_EVENT_SUBSCRIBE(MODULE, encoder_event); +APP_EVENT_SUBSCRIBE(MODULE, module_state_event); +APP_EVENT_SUBSCRIBE(MODULE, theme_rgb_update_event); +APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event); +APP_EVENT_SUBSCRIBE(MODULE, wake_up_event); diff --git a/src/ui/ui_main.c b/src/ui/ui_main.c index 29d160f..1993e26 100644 --- a/src/ui/ui_main.c +++ b/src/ui/ui_main.c @@ -19,6 +19,7 @@ enum { }; struct ui_main_ctx { + lv_obj_t *content; lv_obj_t *status_badges[UI_STATUS_COUNT]; lv_obj_t *status_labels[UI_STATUS_COUNT]; lv_obj_t *battery_icon; @@ -116,6 +117,10 @@ static void ui_main_create_status_chip(lv_obj_t *parent, enum ui_status_id id) void ui_main_refresh_status_bar(const struct ui_main_model *model) { + if (!ui_initialized) { + return; + } + for (uint32_t i = 0; i < UI_STATUS_COUNT; i++) { lv_obj_t *badge = g_ui.status_badges[i]; lv_obj_t *label = g_ui.status_labels[i]; @@ -145,7 +150,8 @@ void ui_main_refresh_battery(const struct ui_main_model *model) lv_color_t battery_color; lv_color_t state_color = lv_color_white(); - if ((g_ui.battery_icon == NULL) || (g_ui.battery_label == NULL) || + if (!ui_initialized || + (g_ui.battery_icon == NULL) || (g_ui.battery_label == NULL) || (g_ui.battery_state_label == NULL)) { return; } @@ -171,7 +177,8 @@ void ui_main_refresh_battery(const struct ui_main_model *model) void ui_main_refresh_datetime(const char *date_text, const char *time_text) { - if ((g_ui.date_label == NULL) || (g_ui.time_label == NULL)) { + if (!ui_initialized || + (g_ui.date_label == NULL) || (g_ui.time_label == NULL)) { return; } @@ -200,6 +207,7 @@ void ui_main_init(const struct ui_main_model *model, lv_obj_t *bottom_row; if (ui_initialized) { + ui_main_refresh_all(model, date_text, time_text); return; } @@ -213,6 +221,7 @@ void ui_main_init(const struct ui_main_model *model, lv_obj_set_scrollbar_mode(screen, LV_SCROLLBAR_MODE_OFF); content = lv_obj_create(screen); + g_ui.content = content; lv_obj_remove_style_all(content); lv_obj_set_size(content, LV_PCT(100), LV_PCT(100)); lv_obj_set_style_bg_opa(content, LV_OPA_TRANSP, 0); @@ -287,3 +296,16 @@ void ui_main_init(const struct ui_main_model *model, ui_main_refresh_all(model, date_text, time_text); ui_initialized = true; } + +void ui_main_set_visible(bool visible) +{ + if (!ui_initialized || (g_ui.content == NULL)) { + 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); + } +} diff --git a/src/ui/ui_main.h b/src/ui/ui_main.h index 921dffe..0443df8 100644 --- a/src/ui/ui_main.h +++ b/src/ui/ui_main.h @@ -31,6 +31,7 @@ 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); #ifdef __cplusplus } diff --git a/src/ui/ui_settings.c b/src/ui/ui_settings.c new file mode 100644 index 0000000..bd330f9 --- /dev/null +++ b/src/ui/ui_settings.c @@ -0,0 +1,316 @@ +#include +#include + +#include + +#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 + +struct ui_settings_row { + lv_obj_t *row; + lv_obj_t *icon; + lv_obj_t *swatch; + lv_obj_t *title; + lv_obj_t *value; +}; + +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 *value; + lv_group_t *focus_group; + bool initialized; +}; + +static struct ui_settings_ctx g_ui; + +static void scroll_set_visible(lv_obj_t *scroll, bool visible) +{ + 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); + } +} + +static void create_scroll(lv_obj_t *parent, lv_obj_t **scroll) +{ + *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); + lv_obj_remove_style_all(row->row); + lv_obj_set_width(row->row, LV_PCT(100)); + lv_obj_set_height(row->row, 30); + lv_obj_set_style_bg_opa(row->row, LV_OPA_TRANSP, 0); + lv_obj_set_style_radius(row->row, 9, 0); + lv_obj_set_style_border_width(row->row, 2, 0); + lv_obj_set_style_border_color(row->row, lv_color_hex(0x0B1017), 0); + lv_obj_set_style_pad_hor(row->row, 6, 0); + lv_obj_set_layout(row->row, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(row->row, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(row->row, LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_column(row->row, 6, 0); + lv_obj_add_flag(row->row, LV_OBJ_FLAG_SCROLL_ON_FOCUS); + lv_group_add_obj(g_ui.focus_group, row->row); + + row->icon = lv_label_create(row->row); + lv_obj_set_width(row->icon, 20); + 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); + lv_obj_set_style_text_color(row->title, lv_color_hex(0xD8DEE9), 0); + lv_label_set_long_mode(row->title, LV_LABEL_LONG_CLIP); + + row->value = lv_label_create(row->row); + lv_obj_set_flex_grow(row->value, 1); + lv_obj_set_style_text_font(row->value, &lv_font_montserrat_14, 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_label_set_long_mode(row->value, LV_LABEL_LONG_CLIP); +} + +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) +{ + lv_label_set_text(row->icon, icon ? icon : ""); + lv_label_set_text(row->title, title ? title : ""); + lv_label_set_text(row->value, value ? 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); + } +} + +static void focus_row(lv_obj_t *scroll, struct ui_settings_row *row, + 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); + } + + focus_row(g_ui.root_scroll, &g_ui.root_rows[state->root_selected], animate); +} + +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(); + + if (g_ui.initialized) { + ui_settings_refresh(state, false); + return; + } + + memset(&g_ui, 0, sizeof(g_ui)); + g_ui.focus_group = lv_group_create(); + + g_ui.content = lv_obj_create(screen); + lv_obj_remove_style_all(g_ui.content); + lv_obj_set_size(g_ui.content, LV_PCT(100), LV_PCT(100)); + lv_obj_set_style_bg_color(g_ui.content, lv_color_hex(0x0B1017), 0); + lv_obj_set_style_bg_opa(g_ui.content, LV_OPA_COVER, 0); + lv_obj_set_style_pad_left(g_ui.content, 14, 0); + lv_obj_set_style_pad_right(g_ui.content, 14, 0); + lv_obj_set_style_pad_top(g_ui.content, 8, 0); + lv_obj_set_style_pad_bottom(g_ui.content, 8, 0); + lv_obj_set_layout(g_ui.content, LV_LAYOUT_FLEX); + lv_obj_set_flex_flow(g_ui.content, LV_FLEX_FLOW_COLUMN); + lv_obj_set_flex_align(g_ui.content, LV_FLEX_ALIGN_START, + LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START); + lv_obj_set_style_pad_row(g_ui.content, 6, 0); + + g_ui.title = lv_label_create(g_ui.content); + lv_obj_set_style_text_font(g_ui.title, &lv_font_montserrat_32, 0); + lv_obj_set_style_text_color(g_ui.title, lv_color_white(), 0); + + 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.value = lv_label_create(g_ui.content); + lv_obj_set_width(g_ui.value, LV_PCT(100)); + lv_obj_set_style_text_font(g_ui.value, &lv_font_montserrat_14, 0); + 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) +{ + lv_color_t accent; + + if ((state == NULL) || !g_ui.initialized) { + return; + } + + accent = lv_color_make(state->accent.r, state->accent.g, state->accent.b); + + scroll_set_visible(g_ui.root_scroll, false); + scroll_set_visible(g_ui.ble_scroll, false); + scroll_set_visible(g_ui.theme_scroll, false); + + 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; + + 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; + } +} + +void ui_settings_set_visible(bool visible) +{ + if (!g_ui.initialized || (g_ui.content == NULL)) { + 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); + } +} diff --git a/src/ui/ui_settings.h b/src/ui/ui_settings.h new file mode 100644 index 0000000..0891f77 --- /dev/null +++ b/src/ui/ui_settings.h @@ -0,0 +1,18 @@ +#ifndef BLINKY_UI_SETTINGS_H_ +#define BLINKY_UI_SETTINGS_H_ + +#include "settings_ui.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); + +#ifdef __cplusplus +} +#endif + +#endif /* BLINKY_UI_SETTINGS_H_ */