feat: implement RootLayout and UIHandler for improved UI structure and app management
This commit is contained in:
220
main/ui/root_layout.cpp
Normal file
220
main/ui/root_layout.cpp
Normal file
@@ -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_obj_t*>(lv_event_get_target(event));
|
||||||
|
UIHandler* handler = static_cast<UIHandler*>(lv_event_get_user_data(event));
|
||||||
|
AppDescriptor* descriptor = static_cast<AppDescriptor*>(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<UIHandler*>(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
201
main/ui/ui_handler.cpp
Normal file
201
main/ui/ui_handler.cpp
Normal file
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user