From 0c26d915653f0a76f74bdd73be1cfa96e384242c Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:39:30 +0800 Subject: [PATCH] feat: implement RootLayout and UIHandler for improved UI structure and app management --- main/ui/root_layout.cpp | 220 ++++++++++++++++++++++++++++++++++++++++ main/ui/ui_handler.cpp | 201 ++++++++++++++++++++++++++++++++++++ 2 files changed, 421 insertions(+) create mode 100644 main/ui/root_layout.cpp create mode 100644 main/ui/ui_handler.cpp diff --git a/main/ui/root_layout.cpp b/main/ui/root_layout.cpp new file mode 100644 index 0000000..fd745a3 --- /dev/null +++ b/main/ui/root_layout.cpp @@ -0,0 +1,220 @@ +#include "ui/root_layout.h" +#include "ui/ui_handler.h" +#include "ui/app_registry.h" +#include "esp_log.h" + +#define TAG "RootLayout" + +// Display dimensions +#define DISPLAY_WIDTH 800 +#define DISPLAY_HEIGHT 480 + +// Layout dimensions +#define HEADER_HEIGHT 40 +#define NAV_BAR_HEIGHT 50 +#define APP_CONTAINER_HEIGHT (DISPLAY_HEIGHT - HEADER_HEIGHT - NAV_BAR_HEIGHT) + +RootLayout::RootLayout(UIHandler* ui_handler) + : _ui_handler(ui_handler) { } + +esp_err_t RootLayout::init(lv_obj_t* parent) { + if (!parent) { + ESP_LOGE(TAG, "Parent object is null"); + return ESP_ERR_INVALID_ARG; + } + + ESP_LOGI(TAG, "Initializing RootLayout"); + + if (create_layout(parent) != ESP_OK) { + ESP_LOGE(TAG, "Failed to create layout"); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "RootLayout initialized successfully"); + return ESP_OK; +} + +esp_err_t RootLayout::deinit(void) { + ESP_LOGI(TAG, "Deinitializing RootLayout"); + + // LVGL will handle cleanup when parent is destroyed + _header = nullptr; + _header_label = nullptr; + _app_container = nullptr; + _nav_bar = nullptr; + _back_button = nullptr; + + return ESP_OK; +} + +esp_err_t RootLayout::create_layout(lv_obj_t* parent) { + // Create header (top) + _header = lv_obj_create(parent); + lv_obj_set_size(_header, DISPLAY_WIDTH, HEADER_HEIGHT); + lv_obj_set_pos(_header, 0, 0); + lv_obj_set_style_bg_color(_header, lv_color_hex(0x333333), 0); + lv_obj_set_style_border_width(_header, 0, 0); + + _header_label = lv_label_create(_header); + lv_label_set_text(_header_label, "App"); + lv_obj_set_style_text_color(_header_label, lv_color_white(), 0); + lv_obj_align(_header_label, LV_ALIGN_LEFT_MID, 10, 0); + + // Create app container (middle) + _app_container = lv_obj_create(parent); + lv_obj_set_size(_app_container, DISPLAY_WIDTH, APP_CONTAINER_HEIGHT); + lv_obj_set_pos(_app_container, 0, HEADER_HEIGHT); + lv_obj_set_style_bg_color(_app_container, lv_color_white(), 0); + lv_obj_set_style_border_width(_app_container, 0, 0); + lv_obj_set_style_pad_all(_app_container, 0, 0); + + // Create navigation bar (bottom) + _nav_bar = lv_obj_create(parent); + lv_obj_set_size(_nav_bar, DISPLAY_WIDTH, NAV_BAR_HEIGHT); + lv_obj_set_pos(_nav_bar, 0, HEADER_HEIGHT + APP_CONTAINER_HEIGHT); + lv_obj_set_style_bg_color(_nav_bar, lv_color_hex(0x333333), 0); + lv_obj_set_style_border_width(_nav_bar, 0, 0); + + ESP_LOGI(TAG, "Layout created: Header=%d, AppContainer=%d, NavBar=%d", + HEADER_HEIGHT, APP_CONTAINER_HEIGHT, NAV_BAR_HEIGHT); + + return ESP_OK; +} + +void RootLayout::update_header(std::string app_name) { + if (!_header_label) { + return; + } + + if (app_name.empty() == false) { + lv_label_set_text(_header_label, app_name.c_str()); + } else { + lv_label_set_text(_header_label, "App"); + } +} + +esp_err_t RootLayout::render_app_icons(void) { + if (!_nav_bar) { + ESP_LOGE(TAG, "Navigation bar not initialized"); + return ESP_FAIL; + } + + // Clear existing nav bar content + lv_obj_clean(_nav_bar); + + // Get all registered apps from registry + const auto& app_descriptors = AppRegistry::instance().get_app_descriptors(); + + if (app_descriptors.empty()) { + ESP_LOGW(TAG, "No apps registered in AppRegistry"); + lv_obj_t* nav_label = lv_label_create(_nav_bar); + lv_label_set_text(nav_label, "No apps available"); + lv_obj_set_style_text_color(nav_label, lv_color_white(), 0); + lv_obj_align(nav_label, LV_ALIGN_CENTER, 0, 0); + return ESP_OK; + } + + ESP_LOGI(TAG, "Rendering %d app icons", app_descriptors.size()); + + // Calculate icon spacing + int icon_count = app_descriptors.size(); + int icon_spacing = DISPLAY_WIDTH / (icon_count + 1); + int x_offset = icon_spacing; + + // Render each app icon + for (size_t i = 0; i < app_descriptors.size(); i++) { + AppDescriptor* descriptor = app_descriptors[i]; + + // Create a container for this app icon + lv_obj_t* icon_container = lv_obj_create(_nav_bar); + lv_obj_set_size(icon_container, icon_spacing - 10, NAV_BAR_HEIGHT - 10); + lv_obj_set_pos(icon_container, x_offset - (icon_spacing - 10) / 2, 5); + lv_obj_set_style_bg_opa(icon_container, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(icon_container, 0, 0); + lv_obj_set_style_pad_all(icon_container, 0, 0); + + // Store both the descriptor and ui_handler as user data + lv_obj_set_user_data(icon_container, descriptor); + + // Let the descriptor draw its icon + descriptor->draw_icon(icon_container); + + // Add click event handler + lv_obj_add_flag(icon_container, LV_OBJ_FLAG_CLICKABLE); + lv_obj_add_event_cb(icon_container, on_app_icon_clicked, LV_EVENT_CLICKED, _ui_handler); + + x_offset += icon_spacing; + } + + // Create back button on the left side of the nav bar + _back_button = lv_btn_create(_nav_bar); + lv_obj_set_size(_back_button, 60, NAV_BAR_HEIGHT - 10); + lv_obj_set_pos(_back_button, 5, 5); + lv_obj_set_style_bg_color(_back_button, lv_color_hex(0x555555), 0); + + // Add back arrow label + lv_obj_t* back_label = lv_label_create(_back_button); + lv_label_set_text(back_label, LV_SYMBOL_LEFT); + lv_obj_set_style_text_color(back_label, lv_color_white(), 0); + lv_obj_align(back_label, LV_ALIGN_CENTER, 0, 0); + + // Add click event handler + lv_obj_add_event_cb(_back_button, on_back_button_clicked, LV_EVENT_CLICKED, _ui_handler); + + // Initially hide back button (shown when app is active) + lv_obj_add_flag(_back_button, LV_OBJ_FLAG_HIDDEN); + + return ESP_OK; +} + +void RootLayout::show_back_button(void) { + if (_back_button) { + lv_obj_clear_flag(_back_button, LV_OBJ_FLAG_HIDDEN); + } +} + +void RootLayout::hide_back_button(void) { + if (_back_button) { + lv_obj_add_flag(_back_button, LV_OBJ_FLAG_HIDDEN); + } +} + +void RootLayout::on_app_icon_clicked(lv_event_t* event) { + lv_obj_t* icon_container = static_cast(lv_event_get_target(event)); + UIHandler* handler = static_cast(lv_event_get_user_data(event)); + AppDescriptor* descriptor = static_cast(lv_obj_get_user_data(icon_container)); + + if (!handler || !descriptor) { + ESP_LOGE(TAG, "Invalid event data in app icon click"); + return; + } + + ESP_LOGI(TAG, "App icon clicked: %s", descriptor->get_name().c_str()); + handler->switch_app(descriptor); +} + +void RootLayout::on_back_button_clicked(lv_event_t* event) { + UIHandler* handler = static_cast(lv_event_get_user_data(event)); + + if (!handler) { + ESP_LOGE(TAG, "Invalid handler in back button click"); + return; + } + + // Get the active app + UIApp* active_app = handler->get_active_app(); + if (!active_app) { + ESP_LOGW(TAG, "Back button pressed but no active app"); + return; + } + + // Let the app handle the back button press + bool handled = active_app->on_back_button_pressed(); + + if (handled) { + ESP_LOGI(TAG, "Back button handled by app: %s", active_app->get_name()); + } else { + ESP_LOGI(TAG, "Back button not handled by app, returning to main screen"); + handler->return_to_main_screen(); + } +} diff --git a/main/ui/ui_handler.cpp b/main/ui/ui_handler.cpp new file mode 100644 index 0000000..e31ebfd --- /dev/null +++ b/main/ui/ui_handler.cpp @@ -0,0 +1,201 @@ +#include "ui/ui_handler.h" +#include "ui/root_layout.h" +#include "ui/app_registry.h" +#include "esp_log.h" + +#define TAG "UIHandler" + +// Display dimensions from constants.h +#define DISPLAY_WIDTH 800 +#define DISPLAY_HEIGHT 480 + +// Layout dimensions +#define HEADER_HEIGHT 40 +#define NAV_BAR_HEIGHT 50 +#define _APP_CONTAINERHEIGHT (DISPLAY_HEIGHT - HEADER_HEIGHT - NAV_BAR_HEIGHT) + +esp_err_t UIHandler::init(void) { + ESP_LOGI(TAG, "Initializing UIHandler"); + + // Create main screen + _main_screen = lv_obj_create(NULL); + if (!_main_screen) { + ESP_LOGE(TAG, "Failed to create main screen"); + return ESP_FAIL; + } + lv_obj_set_style_bg_color(_main_screen, lv_color_black(), 0); + lv_obj_set_size(_main_screen, DISPLAY_WIDTH, DISPLAY_HEIGHT); + + // Create root layout + _root_layout = new RootLayout(this); + if (!_root_layout) { + ESP_LOGE(TAG, "Failed to allocate RootLayout"); + return ESP_FAIL; + } + + if (_root_layout->init(_main_screen) != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize root layout"); + delete _root_layout; + _root_layout = nullptr; + return ESP_FAIL; + } + + // Render app icons from registry + if (_root_layout->render_app_icons() != ESP_OK) { + ESP_LOGW(TAG, "Failed to render app icons"); + } + + // Load the main screen + lv_screen_load(_main_screen); + + ESP_LOGI(TAG, "UIHandler initialized successfully"); + return ESP_OK; +} + +esp_err_t UIHandler::deinit(void) { + ESP_LOGI(TAG, "Deinitializing UIHandler"); + + // Deinit current app + if (_active_app) { + if (_active_app->deinit() != ESP_OK) { + ESP_LOGW(TAG, "Error deinitializing active app: %s", _active_app->get_name()); + } + _active_app = nullptr; + } + + // Delete shutdown app if cached + if (_shutdown_app) { + delete _shutdown_app; + _shutdown_app = nullptr; + } + + // Clean up root layout + if (_root_layout) { + _root_layout->deinit(); + delete _root_layout; + _root_layout = nullptr; + } + + // Main screen will be cleaned up by LVGL + _main_screen = nullptr; + + return ESP_OK; +} + +esp_err_t UIHandler::switch_app(UIApp* app) { + if (!app) { + ESP_LOGE(TAG, "Cannot switch to null app"); + return ESP_ERR_INVALID_ARG; + } + + lv_obj_t* app_container = get_app_container(); + if (!app_container) { + ESP_LOGE(TAG, "App container not initialized"); + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Switching to app: %s", app->get_name()); + + // Deinit current app + if (_active_app) { + if (_active_app->deinit() != ESP_OK) { + ESP_LOGW(TAG, "Error deinitializing app: %s", _active_app->get_name()); + } + } + + // Clear the app container + lv_obj_clean(app_container); + + // Initialize new app + if (app->init(app_container) != ESP_OK) { + ESP_LOGE(TAG, "Failed to initialize app: %s", app->get_name()); + _active_app = nullptr; + return ESP_FAIL; + } + + _active_app = app; + + // Update header through RootLayout + if (_root_layout) { + _root_layout->update_header(_active_app->get_name()); + _root_layout->show_back_button(); + } + + return ESP_OK; +} + +esp_err_t UIHandler::switch_app(AppDescriptor* app_descriptor) { + if (!app_descriptor) { + ESP_LOGE(TAG, "Cannot switch to null app descriptor"); + return ESP_ERR_INVALID_ARG; + } + + UIApp* app = app_descriptor->get_app_instance(); + if (!app) { + ESP_LOGE(TAG, "App descriptor has null app instance"); + return ESP_ERR_INVALID_ARG; + } + + return switch_app(app); +} + +void UIHandler::route_event(uint32_t event_type, void* event_data) { + if (_active_app) { + _active_app->handle_event(event_type, event_data); + } +} + +esp_err_t UIHandler::show_shutdown_screen(std::string message) { + ESP_LOGI(TAG, "Showing shutdown screen"); + + lv_obj_t* app_container = get_app_container(); + if (!app_container) { + ESP_LOGE(TAG, "App container not initialized"); + return ESP_FAIL; + } + + // Clear current app reference + _active_app = nullptr; + + // Clear the app container + lv_obj_clean(app_container); + + // Create shutdown message + lv_obj_t* shutdown_label = lv_label_create(app_container); + lv_label_set_text(shutdown_label, message.empty() ? "Shutting down..." : message.c_str()); + lv_obj_set_style_text_color(shutdown_label, lv_color_white(), 0); + lv_obj_align(shutdown_label, LV_ALIGN_CENTER, 0, 0); + + // Update header through RootLayout + if (_root_layout) { + _root_layout->update_header("System Shutdown"); + } + + return ESP_OK; +} + +esp_err_t UIHandler::return_to_main_screen(void) { + ESP_LOGI(TAG, "Returning to main screen"); + + // Deinit current app + if (_active_app) { + if (_active_app->deinit() != ESP_OK) { + ESP_LOGW(TAG, "Error deinitializing app: %s", _active_app->get_name()); + } + _active_app = nullptr; + } + + // Clear the app container + lv_obj_t* app_container = get_app_container(); + if (app_container) { + lv_obj_clean(app_container); + } + + // Update header and hide back button through RootLayout + if (_root_layout) { + _root_layout->update_header(""); + _root_layout->hide_back_button(); + } + + return ESP_OK; +}