Refactor RootLayout and UIHandler for improved structure and functionality

- Updated RootLayout to manage layout initialization and deinitialization more effectively.
- Removed unnecessary dependencies and streamlined event handling for keyboard events.
- Enhanced UIHandler to utilize shared pointers for app descriptors, improving memory management.
- Added methods for showing and hiding navigation elements in RootLayout.
- Introduced textarea widget with instant response by disabling animations.
- Improved error handling and logging throughout the UI components.
This commit is contained in:
GW_MC
2026-02-01 13:03:56 +08:00
parent 237a3a96c5
commit 06e81301b2
22 changed files with 880 additions and 2198 deletions

View File

@@ -1,123 +1,93 @@
#include "ui/root_layout.h"
#include "ui/ui_handler.h"
#include "ui/app_registry.h"
#include "ui/events.h"
#include "esp_log.h"
#include "esp_event.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)
// forward-declare local event callback
static void on_home_button_clicked(lv_event_t* event);
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;
RootLayout::~RootLayout() {
deinit();
}
esp_err_t RootLayout::deinit(void) {
ESP_LOGI(TAG, "Deinitializing RootLayout");
esp_err_t RootLayout::init(lv_obj_t* parent, UIHandler* ui_handler) {
// 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) {
// Configure parent as flexbox column layout
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(parent, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_style_pad_all(parent, 0, 0);
lv_obj_set_style_pad_gap(parent, 0, 0);
//
// Create header (top, fixed height)
_header = lv_obj_create(parent);
lv_obj_set_width(_header, lv_pct(100));
lv_obj_set_height(_header, HEADER_HEIGHT);
lv_obj_set_style_bg_color(_header, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_border_width(_header, 0, 0);
lv_obj_set_style_border_color(_header, lv_color_hex(0x000000), 0);
lv_obj_set_style_border_width(_header, 1, LV_BORDER_SIDE_BOTTOM);
lv_obj_set_style_pad_all(_header, 0, 0);
lv_obj_set_style_radius(_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_black(), 0);
lv_obj_align(_header_label, LV_ALIGN_LEFT_MID, 10, 0);
// Create app container (middle, flexible - grows to fill available space)
_app_container = lv_obj_create(parent);
lv_obj_set_width(_app_container, lv_pct(100));
lv_obj_set_flex_grow(_app_container, 1);
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);
lv_obj_set_style_radius(_app_container, 0, 0);
header_obj_ = lv_obj_create(parent);
lv_obj_set_width(header_obj_, lv_pct(100));
lv_obj_set_height(header_obj_, HEADER_HEIGHT);
lv_obj_set_style_bg_color(header_obj_, lv_color_white(), 0);
lv_obj_set_style_border_width(header_obj_, 0, 0);
lv_obj_set_style_border_color(header_obj_, lv_color_black(), 0);
lv_obj_set_style_border_width(header_obj_, 1, LV_BORDER_SIDE_BOTTOM);
lv_obj_set_style_pad_all(header_obj_, 0, 0);
lv_obj_set_style_radius(header_obj_, 0, 0);
//
header_label_ = lv_label_create(header_obj_);
lv_label_set_text(header_label_, "App");
lv_obj_set_style_text_color(header_label_, lv_color_black(), 0);
lv_obj_align(header_label_, LV_ALIGN_LEFT_MID, 10, 0);
//
// Create app container (middle, flexible height)
app_container_ = lv_obj_create(parent);
lv_obj_set_width(app_container_, lv_pct(100));
lv_obj_set_flex_grow(app_container_, 1);
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);
lv_obj_set_style_radius(app_container_, 0, 0);
//
// Create navigation bar (bottom, fixed height)
_nav_bar = lv_obj_create(parent);
lv_obj_set_width(_nav_bar, lv_pct(100));
lv_obj_set_height(_nav_bar, NAV_BAR_HEIGHT);
lv_obj_set_style_bg_color(_nav_bar, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_border_color(_nav_bar, lv_color_hex(0x000000), 0);
lv_obj_set_style_border_width(_nav_bar, 1, LV_BORDER_SIDE_TOP);
lv_obj_set_style_pad_all(_nav_bar, 5, 0);
lv_obj_set_style_radius(_nav_bar, 0, 0);
nav_bar_obj_ = lv_obj_create(parent);
lv_obj_set_width(nav_bar_obj_, lv_pct(100));
lv_obj_set_height(nav_bar_obj_, NAV_BAR_HEIGHT);
lv_obj_set_style_bg_color(nav_bar_obj_, lv_color_white(), 0);
lv_obj_set_style_border_color(nav_bar_obj_, lv_color_black(), 0);
lv_obj_set_style_border_width(nav_bar_obj_, 1, LV_BORDER_SIDE_TOP);
lv_obj_set_style_pad_all(nav_bar_obj_, 5, 0);
lv_obj_set_style_radius(nav_bar_obj_, 0, 0);
// Configure nav bar as flexbox row layout with space-between
lv_obj_set_flex_flow(_nav_bar, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(_nav_bar, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
// Create back button (aligned to start by flex layout)
_back_button = lv_btn_create(_nav_bar);
lv_obj_set_size(_back_button, 60, NAV_BAR_HEIGHT - 10);
lv_obj_set_style_bg_color(_back_button, lv_color_hex(0x555555), 0);
lv_obj_add_event_cb(_back_button, on_back_button_clicked, LV_EVENT_CLICKED, _ui_handler);
lv_obj_add_flag(_back_button, LV_OBJ_FLAG_HIDDEN);
// Add back arrow label
lv_obj_t* back_label = lv_label_create(_back_button);
back_button_ = lv_btn_create(nav_bar_obj_);
lv_obj_set_size(back_button_, 60, NAV_BAR_HEIGHT - 10);
lv_obj_set_style_bg_color(back_button_, lv_color_white(), 0);
lv_obj_add_flag(back_button_, LV_OBJ_FLAG_HIDDEN);
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_black(), 0);
lv_obj_align(back_label, LV_ALIGN_CENTER, 0, 0);
// Create home button (aligned to end by flex layout)
lv_obj_t* home_button = lv_btn_create(_nav_bar);
lv_obj_set_size(home_button, 60, NAV_BAR_HEIGHT - 10);
lv_obj_set_style_bg_color(home_button, lv_color_hex(0x555555), 0);
lv_obj_t* home_label = lv_label_create(home_button);
home_button_ = lv_btn_create(nav_bar_obj_);
lv_obj_set_size(home_button_, 60, NAV_BAR_HEIGHT - 10);
lv_obj_set_style_bg_color(home_button_, lv_color_white(), 0);
lv_obj_t* home_label = lv_label_create(home_button_);
lv_label_set_text(home_label, LV_SYMBOL_HOME);
lv_obj_set_style_text_color(home_label, lv_color_white(), 0);
lv_obj_set_style_text_color(home_label, lv_color_black(), 0);
lv_obj_align(home_label, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_event_cb(home_button, on_home_button_clicked, LV_EVENT_CLICKED, _ui_handler);
// Register keyboard event handler
esp_err_t err = esp_event_handler_instance_register(
UI_EVENT_BASE,
ESP_EVENT_ANY_ID,
[](void* handler_args, esp_event_base_t base, int32_t id, void* event_data) {
RootLayout* root_layout = static_cast<RootLayout*>(handler_args);
root_layout->on_keyboard_event_(handler_args, base, id, event_data);
},
this,
&keyboard_event_handler_instance_
);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to register keyboard event handler: %s", esp_err_to_name(err));
}
ESP_LOGI(TAG, "Layout created with flexible design: Header=%d, NavBar=%d",
HEADER_HEIGHT, NAV_BAR_HEIGHT);
@@ -125,140 +95,155 @@ esp_err_t RootLayout::create_layout(lv_obj_t* parent) {
return ESP_OK;
}
void RootLayout::update_header(std::string app_name) {
if (!_header_label) {
return;
esp_err_t RootLayout::deinit(void) {
// Unregister keyboard event handler
if (keyboard_event_handler_instance_) {
esp_event_handler_instance_unregister(
UI_EVENT_BASE,
ESP_EVENT_ANY_ID,
keyboard_event_handler_instance_
);
keyboard_event_handler_instance_ = nullptr;
}
if (app_name.empty() == false) {
lv_label_set_text(_header_label, app_name.c_str());
header_obj_ = nullptr;
header_label_ = nullptr;
//
app_container_ = nullptr;
//
nav_bar_obj_ = nullptr;
back_button_ = nullptr;
home_button_ = nullptr;
return ESP_OK;
}
void RootLayout::hide_nav_bar(void) const {
if (nav_bar_obj_) {
lv_obj_add_flag(nav_bar_obj_, LV_OBJ_FLAG_HIDDEN);
} else {
lv_label_set_text(_header_label, "App");
ESP_LOGW(TAG, "Navigation bar not initialized");
}
}
esp_err_t RootLayout::render_app_icons(void) {
if (!_nav_bar) {
ESP_LOGE(TAG, "Navigation bar not initialized");
void RootLayout::show_nav_bar(void) const {
if (nav_bar_obj_) {
lv_obj_clear_flag(nav_bar_obj_, LV_OBJ_FLAG_HIDDEN);
} else {
ESP_LOGW(TAG, "Navigation bar not initialized");
}
}
void RootLayout::show_back_button(void) const {
if (back_button_) {
lv_obj_clear_flag(back_button_, LV_OBJ_FLAG_HIDDEN);
} else {
ESP_LOGW(TAG, "Back button not initialized");
}
}
void RootLayout::show_home_button(void) const {
if (home_button_) {
lv_obj_clear_flag(home_button_, LV_OBJ_FLAG_HIDDEN);
} else {
ESP_LOGW(TAG, "Home button not found in navigation bar");
}
}
void RootLayout::hide_back_button(void) const {
if (back_button_) {
lv_obj_add_flag(back_button_, LV_OBJ_FLAG_HIDDEN);
} else {
ESP_LOGW(TAG, "Back button not initialized");
}
}
void RootLayout::hide_home_button(void) const {
if (home_button_) {
lv_obj_add_flag(home_button_, LV_OBJ_FLAG_HIDDEN);
} else {
ESP_LOGW(TAG, "Home button not found in navigation bar");
}
}
esp_err_t RootLayout::register_back_button_callback(lv_event_cb_t callback, void* user_data, lv_event_dsc_t** out_event_dsc) const {
if (!back_button_) {
ESP_LOGE(TAG, "Back button not initialized");
return ESP_ERR_INVALID_STATE;
}
if (!callback) {
ESP_LOGE(TAG, "Invalid argument: callback is nullptr");
return ESP_ERR_INVALID_ARG;
}
if (out_event_dsc == nullptr) {
ESP_LOGE(TAG, "Invalid argument: out_event_dsc is nullptr");
return ESP_ERR_INVALID_ARG;
}
*out_event_dsc = lv_obj_add_event_cb(back_button_, callback, LV_EVENT_CLICKED, user_data);
if (*out_event_dsc == nullptr) {
ESP_LOGE(TAG, "Failed to register back button callback");
return ESP_FAIL;
}
// Clear existing app container content (icons are rendered in the app area)
if (!_app_container) {
ESP_LOGE(TAG, "App container not initialized");
return ESP_FAIL;
}
lv_obj_clean(_app_container);
// 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", (int)app_descriptors.size());
// Calculate icon spacing inside the app container
int icon_count = app_descriptors.size();
int icon_width = 96;
int icon_height = 96;
int icon_spacing = DISPLAY_WIDTH / (icon_count + 1);
int x_offset = icon_spacing;
int y_offset = (APP_CONTAINER_HEIGHT - icon_height) / 2;
// Render each app icon into the app container
for (size_t i = 0; i < app_descriptors.size(); i++) {
AppDescriptor* descriptor = app_descriptors[i];
lv_obj_t* icon_container = lv_obj_create(_app_container);
lv_obj_set_size(icon_container, icon_width, icon_height);
lv_obj_set_pos(icon_container, x_offset - icon_width / 2, y_offset);
lv_obj_set_style_bg_opa(icon_container, LV_OPA_TRANSP, 0);
lv_obj_set_style_pad_all(icon_container, 0, 0);
// add a border for debugging
lv_obj_set_style_border_color(icon_container, lv_color_hex(0x000000), 0);
lv_obj_set_style_border_width(icon_container, 1, 0);
lv_obj_set_user_data(icon_container, descriptor);
descriptor->draw_icon(icon_container);
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;
}
return ESP_OK;
}
void RootLayout::show_back_button(void) {
if (_back_button) {
lv_obj_clear_flag(_back_button, LV_OBJ_FLAG_HIDDEN);
esp_err_t RootLayout::register_home_button_callback(lv_event_cb_t callback, void* user_data, lv_event_dsc_t** out_event_dsc) const {
if (!home_button_) {
ESP_LOGE(TAG, "Home button not found in navigation bar");
return ESP_ERR_NOT_FOUND;
}
if (!callback) {
ESP_LOGE(TAG, "Invalid argument: callback is nullptr");
return ESP_ERR_INVALID_ARG;
}
if (out_event_dsc == nullptr) {
ESP_LOGE(TAG, "Invalid argument: out_event_dsc is nullptr");
return ESP_ERR_INVALID_ARG;
}
*out_event_dsc = lv_obj_add_event_cb(home_button_, callback, LV_EVENT_CLICKED, user_data);
if (*out_event_dsc == nullptr) {
ESP_LOGE(TAG, "Failed to register home button callback");
return ESP_FAIL;
}
return ESP_OK;
}
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) {
// Use the current target (the object the callback was attached to)
// instead of the event target, which may be a child (like a label).
lv_obj_t* icon_container = static_cast<lv_obj_t*>(lv_event_get_current_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_err_t RootLayout::update_header(const std::string& title) const {
if (!header_label_) {
return ESP_ERR_INVALID_STATE;
}
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());
if (title.empty() == false) {
lv_label_set_text(header_label_, title.c_str());
} else {
ESP_LOGI(TAG, "Back button not handled by app, returning to main screen");
handler->return_to_main_screen();
lv_label_set_text(header_label_, "App");
}
return ESP_OK;
}
static void on_home_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 home button click");
void RootLayout::on_keyboard_event_(void* handler_args, esp_event_base_t base, int32_t id, void* event_data) {
if (base != UI_EVENT_BASE) {
return;
}
handler->return_to_main_screen();
switch (id) {
case UI_EVENT_KEYBOARD_SHOWN:
hide_nav_bar();
break;
case UI_EVENT_KEYBOARD_HIDDEN:
show_nav_bar();
break;
default:
ESP_LOGW(TAG, "Unknown keyboard event ID: %ld", id);
break;
}
}