mirror of
https://github.com/78/xiaozhi-esp32.git
synced 2026-02-27 14:26:36 +00:00
* Initial plan * Fix LVGL object deletion issue in SetChatMessage - Refresh child_count after deleting first_child to avoid using stale count - Add lv_obj_is_valid() checks before accessing deleted objects - Prevent duplicate deletion by refreshing child_count before system message deletion - Ensures last_child validation before scrolling to it Co-authored-by: 78 <4488133+78@users.noreply.github.com> * Address code review feedback on LVGL object deletion fix - Move last_child retrieval after deletion to avoid stale pointer - Add child_count > 0 check before scrolling to prevent edge case issues - Add lv_obj_is_valid() check for last_bubble for consistency Co-authored-by: 78 <4488133+78@users.noreply.github.com> * Final review: LVGL object deletion fix complete Co-authored-by: 78 <4488133+78@users.noreply.github.com> * Remove std::thread wrappers in display callbacks to fix thread safety Remove detached threads that were calling SetChatMessage from download/upgrade progress callbacks. The DisplayLockGuard mutex already provides thread safety, so we can call display methods directly from the callback thread without spawning additional threads, which were causing race conditions. Co-authored-by: 78 <4488133+78@users.noreply.github.com> * Use Application::Schedule() for display updates in callbacks Instead of calling SetChatMessage directly from download/upgrade progress callbacks, queue the updates using Application::Schedule(). This prevents blocking the network receive task and avoids potential UART FIFO overflow issues with 4G modems. The scheduled callbacks execute in the main task thread, maintaining proper thread safety. Co-authored-by: 78 <4488133+78@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: 78 <4488133+78@users.noreply.github.com>
1203 lines
48 KiB
C++
1203 lines
48 KiB
C++
#include "lcd_display.h"
|
|
#include "gif/lvgl_gif.h"
|
|
#include "settings.h"
|
|
#include "lvgl_theme.h"
|
|
#include "assets/lang_config.h"
|
|
|
|
#include <vector>
|
|
#include <algorithm>
|
|
#include <font_awesome.h>
|
|
#include <esp_log.h>
|
|
#include <esp_err.h>
|
|
#include <esp_lvgl_port.h>
|
|
#include <esp_psram.h>
|
|
#include <cstring>
|
|
|
|
#include "board.h"
|
|
|
|
#define TAG "LcdDisplay"
|
|
|
|
LV_FONT_DECLARE(BUILTIN_TEXT_FONT);
|
|
LV_FONT_DECLARE(BUILTIN_ICON_FONT);
|
|
LV_FONT_DECLARE(font_awesome_30_4);
|
|
|
|
void LcdDisplay::InitializeLcdThemes() {
|
|
auto text_font = std::make_shared<LvglBuiltInFont>(&BUILTIN_TEXT_FONT);
|
|
auto icon_font = std::make_shared<LvglBuiltInFont>(&BUILTIN_ICON_FONT);
|
|
auto large_icon_font = std::make_shared<LvglBuiltInFont>(&font_awesome_30_4);
|
|
|
|
// light theme
|
|
auto light_theme = new LvglTheme("light");
|
|
light_theme->set_background_color(lv_color_hex(0xFFFFFF));
|
|
light_theme->set_text_color(lv_color_hex(0x000000));
|
|
light_theme->set_chat_background_color(lv_color_hex(0xE0E0E0));
|
|
light_theme->set_user_bubble_color(lv_color_hex(0x00FF00));
|
|
light_theme->set_assistant_bubble_color(lv_color_hex(0xDDDDDD));
|
|
light_theme->set_system_bubble_color(lv_color_hex(0xFFFFFF));
|
|
light_theme->set_system_text_color(lv_color_hex(0x000000));
|
|
light_theme->set_border_color(lv_color_hex(0x000000));
|
|
light_theme->set_low_battery_color(lv_color_hex(0x000000));
|
|
light_theme->set_text_font(text_font);
|
|
light_theme->set_icon_font(icon_font);
|
|
light_theme->set_large_icon_font(large_icon_font);
|
|
|
|
// dark theme
|
|
auto dark_theme = new LvglTheme("dark");
|
|
dark_theme->set_background_color(lv_color_hex(0x000000));
|
|
dark_theme->set_text_color(lv_color_hex(0xFFFFFF));
|
|
dark_theme->set_chat_background_color(lv_color_hex(0x1F1F1F));
|
|
dark_theme->set_user_bubble_color(lv_color_hex(0x00FF00));
|
|
dark_theme->set_assistant_bubble_color(lv_color_hex(0x222222));
|
|
dark_theme->set_system_bubble_color(lv_color_hex(0x000000));
|
|
dark_theme->set_system_text_color(lv_color_hex(0xFFFFFF));
|
|
dark_theme->set_border_color(lv_color_hex(0xFFFFFF));
|
|
dark_theme->set_low_battery_color(lv_color_hex(0xFF0000));
|
|
dark_theme->set_text_font(text_font);
|
|
dark_theme->set_icon_font(icon_font);
|
|
dark_theme->set_large_icon_font(large_icon_font);
|
|
|
|
auto& theme_manager = LvglThemeManager::GetInstance();
|
|
theme_manager.RegisterTheme("light", light_theme);
|
|
theme_manager.RegisterTheme("dark", dark_theme);
|
|
}
|
|
|
|
LcdDisplay::LcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel, int width, int height)
|
|
: panel_io_(panel_io), panel_(panel) {
|
|
width_ = width;
|
|
height_ = height;
|
|
|
|
// Initialize LCD themes
|
|
InitializeLcdThemes();
|
|
|
|
// Load theme from settings
|
|
Settings settings("display", false);
|
|
std::string theme_name = settings.GetString("theme", "light");
|
|
current_theme_ = LvglThemeManager::GetInstance().GetTheme(theme_name);
|
|
|
|
// Create a timer to hide the preview image
|
|
esp_timer_create_args_t preview_timer_args = {
|
|
.callback = [](void* arg) {
|
|
LcdDisplay* display = static_cast<LcdDisplay*>(arg);
|
|
display->SetPreviewImage(nullptr);
|
|
},
|
|
.arg = this,
|
|
.dispatch_method = ESP_TIMER_TASK,
|
|
.name = "preview_timer",
|
|
.skip_unhandled_events = false,
|
|
};
|
|
esp_timer_create(&preview_timer_args, &preview_timer_);
|
|
}
|
|
|
|
SpiLcdDisplay::SpiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
|
|
int width, int height, int offset_x, int offset_y, bool mirror_x, bool mirror_y, bool swap_xy)
|
|
: LcdDisplay(panel_io, panel, width, height) {
|
|
|
|
// draw white
|
|
std::vector<uint16_t> buffer(width_, 0xFFFF);
|
|
for (int y = 0; y < height_; y++) {
|
|
esp_lcd_panel_draw_bitmap(panel_, 0, y, width_, y + 1, buffer.data());
|
|
}
|
|
|
|
// Set the display to on
|
|
ESP_LOGI(TAG, "Turning display on");
|
|
{
|
|
esp_err_t __err = esp_lcd_panel_disp_on_off(panel_, true);
|
|
if (__err == ESP_ERR_NOT_SUPPORTED) {
|
|
ESP_LOGW(TAG, "Panel does not support disp_on_off; assuming ON");
|
|
} else {
|
|
ESP_ERROR_CHECK(__err);
|
|
}
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Initialize LVGL library");
|
|
lv_init();
|
|
|
|
#if CONFIG_SPIRAM
|
|
// lv image cache, currently only PNG is supported
|
|
size_t psram_size_mb = esp_psram_get_size() / 1024 / 1024;
|
|
if (psram_size_mb >= 8) {
|
|
lv_image_cache_resize(2 * 1024 * 1024, true);
|
|
ESP_LOGI(TAG, "Use 2MB of PSRAM for image cache");
|
|
} else if (psram_size_mb >= 2) {
|
|
lv_image_cache_resize(512 * 1024, true);
|
|
ESP_LOGI(TAG, "Use 512KB of PSRAM for image cache");
|
|
}
|
|
#endif
|
|
|
|
ESP_LOGI(TAG, "Initialize LVGL port");
|
|
lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG();
|
|
port_cfg.task_priority = 1;
|
|
#if CONFIG_SOC_CPU_CORES_NUM > 1
|
|
port_cfg.task_affinity = 1;
|
|
#endif
|
|
lvgl_port_init(&port_cfg);
|
|
|
|
ESP_LOGI(TAG, "Adding LCD display");
|
|
const lvgl_port_display_cfg_t display_cfg = {
|
|
.io_handle = panel_io_,
|
|
.panel_handle = panel_,
|
|
.control_handle = nullptr,
|
|
.buffer_size = static_cast<uint32_t>(width_ * 20),
|
|
.double_buffer = false,
|
|
.trans_size = 0,
|
|
.hres = static_cast<uint32_t>(width_),
|
|
.vres = static_cast<uint32_t>(height_),
|
|
.monochrome = false,
|
|
.rotation = {
|
|
.swap_xy = swap_xy,
|
|
.mirror_x = mirror_x,
|
|
.mirror_y = mirror_y,
|
|
},
|
|
.color_format = LV_COLOR_FORMAT_RGB565,
|
|
.flags = {
|
|
.buff_dma = 1,
|
|
.buff_spiram = 0,
|
|
.sw_rotate = 0,
|
|
.swap_bytes = 1,
|
|
.full_refresh = 0,
|
|
.direct_mode = 0,
|
|
},
|
|
};
|
|
|
|
display_ = lvgl_port_add_disp(&display_cfg);
|
|
if (display_ == nullptr) {
|
|
ESP_LOGE(TAG, "Failed to add display");
|
|
return;
|
|
}
|
|
|
|
if (offset_x != 0 || offset_y != 0) {
|
|
lv_display_set_offset(display_, offset_x, offset_y);
|
|
}
|
|
|
|
SetupUI();
|
|
}
|
|
|
|
|
|
// RGB LCD implementation
|
|
RgbLcdDisplay::RgbLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
|
|
int width, int height, int offset_x, int offset_y,
|
|
bool mirror_x, bool mirror_y, bool swap_xy)
|
|
: LcdDisplay(panel_io, panel, width, height) {
|
|
|
|
// draw white
|
|
std::vector<uint16_t> buffer(width_, 0xFFFF);
|
|
for (int y = 0; y < height_; y++) {
|
|
esp_lcd_panel_draw_bitmap(panel_, 0, y, width_, y + 1, buffer.data());
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Initialize LVGL library");
|
|
lv_init();
|
|
|
|
ESP_LOGI(TAG, "Initialize LVGL port");
|
|
lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG();
|
|
port_cfg.task_priority = 1;
|
|
port_cfg.timer_period_ms = 50;
|
|
lvgl_port_init(&port_cfg);
|
|
|
|
ESP_LOGI(TAG, "Adding LCD display");
|
|
const lvgl_port_display_cfg_t display_cfg = {
|
|
.io_handle = panel_io_,
|
|
.panel_handle = panel_,
|
|
.buffer_size = static_cast<uint32_t>(width_ * 20),
|
|
.double_buffer = true,
|
|
.hres = static_cast<uint32_t>(width_),
|
|
.vres = static_cast<uint32_t>(height_),
|
|
.rotation = {
|
|
.swap_xy = swap_xy,
|
|
.mirror_x = mirror_x,
|
|
.mirror_y = mirror_y,
|
|
},
|
|
.flags = {
|
|
.buff_dma = 1,
|
|
.swap_bytes = 0,
|
|
.full_refresh = 1,
|
|
.direct_mode = 1,
|
|
},
|
|
};
|
|
|
|
const lvgl_port_display_rgb_cfg_t rgb_cfg = {
|
|
.flags = {
|
|
.bb_mode = true,
|
|
.avoid_tearing = true,
|
|
}
|
|
};
|
|
|
|
display_ = lvgl_port_add_disp_rgb(&display_cfg, &rgb_cfg);
|
|
if (display_ == nullptr) {
|
|
ESP_LOGE(TAG, "Failed to add RGB display");
|
|
return;
|
|
}
|
|
|
|
if (offset_x != 0 || offset_y != 0) {
|
|
lv_display_set_offset(display_, offset_x, offset_y);
|
|
}
|
|
|
|
SetupUI();
|
|
}
|
|
|
|
MipiLcdDisplay::MipiLcdDisplay(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_handle_t panel,
|
|
int width, int height, int offset_x, int offset_y,
|
|
bool mirror_x, bool mirror_y, bool swap_xy)
|
|
: LcdDisplay(panel_io, panel, width, height) {
|
|
|
|
ESP_LOGI(TAG, "Initialize LVGL library");
|
|
lv_init();
|
|
|
|
ESP_LOGI(TAG, "Initialize LVGL port");
|
|
lvgl_port_cfg_t port_cfg = ESP_LVGL_PORT_INIT_CONFIG();
|
|
lvgl_port_init(&port_cfg);
|
|
|
|
ESP_LOGI(TAG, "Adding LCD display");
|
|
const lvgl_port_display_cfg_t disp_cfg = {
|
|
.io_handle = panel_io,
|
|
.panel_handle = panel,
|
|
.control_handle = nullptr,
|
|
.buffer_size = static_cast<uint32_t>(width_ * 50),
|
|
.double_buffer = false,
|
|
.hres = static_cast<uint32_t>(width_),
|
|
.vres = static_cast<uint32_t>(height_),
|
|
.monochrome = false,
|
|
/* Rotation values must be same as used in esp_lcd for initial settings of the screen */
|
|
.rotation = {
|
|
.swap_xy = swap_xy,
|
|
.mirror_x = mirror_x,
|
|
.mirror_y = mirror_y,
|
|
},
|
|
.flags = {
|
|
.buff_dma = true,
|
|
.buff_spiram =false,
|
|
.sw_rotate = true,
|
|
},
|
|
};
|
|
|
|
const lvgl_port_display_dsi_cfg_t dpi_cfg = {
|
|
.flags = {
|
|
.avoid_tearing = false,
|
|
}
|
|
};
|
|
display_ = lvgl_port_add_disp_dsi(&disp_cfg, &dpi_cfg);
|
|
if (display_ == nullptr) {
|
|
ESP_LOGE(TAG, "Failed to add display");
|
|
return;
|
|
}
|
|
|
|
if (offset_x != 0 || offset_y != 0) {
|
|
lv_display_set_offset(display_, offset_x, offset_y);
|
|
}
|
|
|
|
SetupUI();
|
|
}
|
|
|
|
LcdDisplay::~LcdDisplay() {
|
|
SetPreviewImage(nullptr);
|
|
|
|
// Clean up GIF controller
|
|
if (gif_controller_) {
|
|
gif_controller_->Stop();
|
|
gif_controller_.reset();
|
|
}
|
|
|
|
if (preview_timer_ != nullptr) {
|
|
esp_timer_stop(preview_timer_);
|
|
esp_timer_delete(preview_timer_);
|
|
}
|
|
|
|
if (preview_image_ != nullptr) {
|
|
lv_obj_del(preview_image_);
|
|
}
|
|
if (chat_message_label_ != nullptr) {
|
|
lv_obj_del(chat_message_label_);
|
|
}
|
|
if (emoji_label_ != nullptr) {
|
|
lv_obj_del(emoji_label_);
|
|
}
|
|
if (emoji_image_ != nullptr) {
|
|
lv_obj_del(emoji_image_);
|
|
}
|
|
if (emoji_box_ != nullptr) {
|
|
lv_obj_del(emoji_box_);
|
|
}
|
|
if (content_ != nullptr) {
|
|
lv_obj_del(content_);
|
|
}
|
|
if (bottom_bar_ != nullptr) {
|
|
lv_obj_del(bottom_bar_);
|
|
}
|
|
if (status_bar_ != nullptr) {
|
|
lv_obj_del(status_bar_);
|
|
}
|
|
if (top_bar_ != nullptr) {
|
|
lv_obj_del(top_bar_);
|
|
}
|
|
if (side_bar_ != nullptr) {
|
|
lv_obj_del(side_bar_);
|
|
}
|
|
if (container_ != nullptr) {
|
|
lv_obj_del(container_);
|
|
}
|
|
if (display_ != nullptr) {
|
|
lv_display_delete(display_);
|
|
}
|
|
|
|
if (panel_ != nullptr) {
|
|
esp_lcd_panel_del(panel_);
|
|
}
|
|
if (panel_io_ != nullptr) {
|
|
esp_lcd_panel_io_del(panel_io_);
|
|
}
|
|
}
|
|
|
|
bool LcdDisplay::Lock(int timeout_ms) {
|
|
return lvgl_port_lock(timeout_ms);
|
|
}
|
|
|
|
void LcdDisplay::Unlock() {
|
|
lvgl_port_unlock();
|
|
}
|
|
|
|
#if CONFIG_USE_WECHAT_MESSAGE_STYLE
|
|
void LcdDisplay::SetupUI() {
|
|
DisplayLockGuard lock(this);
|
|
|
|
auto lvgl_theme = static_cast<LvglTheme*>(current_theme_);
|
|
auto text_font = lvgl_theme->text_font()->font();
|
|
auto icon_font = lvgl_theme->icon_font()->font();
|
|
auto large_icon_font = lvgl_theme->large_icon_font()->font();
|
|
|
|
auto screen = lv_screen_active();
|
|
lv_obj_set_style_text_font(screen, text_font, 0);
|
|
lv_obj_set_style_text_color(screen, lvgl_theme->text_color(), 0);
|
|
lv_obj_set_style_bg_color(screen, lvgl_theme->background_color(), 0);
|
|
|
|
/* Container */
|
|
container_ = lv_obj_create(screen);
|
|
lv_obj_set_size(container_, LV_HOR_RES, LV_VER_RES);
|
|
lv_obj_set_style_radius(container_, 0, 0);
|
|
lv_obj_set_flex_flow(container_, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_style_pad_all(container_, 0, 0);
|
|
lv_obj_set_style_border_width(container_, 0, 0);
|
|
lv_obj_set_style_pad_row(container_, 0, 0);
|
|
lv_obj_set_style_bg_color(container_, lvgl_theme->background_color(), 0);
|
|
lv_obj_set_style_border_color(container_, lvgl_theme->border_color(), 0);
|
|
|
|
/* Layer 1: Top bar - for status icons */
|
|
top_bar_ = lv_obj_create(container_);
|
|
lv_obj_set_size(top_bar_, LV_HOR_RES, LV_SIZE_CONTENT);
|
|
lv_obj_set_style_radius(top_bar_, 0, 0);
|
|
lv_obj_set_style_bg_opa(top_bar_, LV_OPA_50, 0); // 50% opacity background
|
|
lv_obj_set_style_bg_color(top_bar_, lvgl_theme->background_color(), 0);
|
|
lv_obj_set_style_border_width(top_bar_, 0, 0);
|
|
lv_obj_set_style_pad_all(top_bar_, 0, 0);
|
|
lv_obj_set_style_pad_top(top_bar_, lvgl_theme->spacing(2), 0);
|
|
lv_obj_set_style_pad_bottom(top_bar_, lvgl_theme->spacing(2), 0);
|
|
lv_obj_set_style_pad_left(top_bar_, lvgl_theme->spacing(4), 0);
|
|
lv_obj_set_style_pad_right(top_bar_, lvgl_theme->spacing(4), 0);
|
|
lv_obj_set_flex_flow(top_bar_, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(top_bar_, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_scrollbar_mode(top_bar_, LV_SCROLLBAR_MODE_OFF);
|
|
|
|
// Left icon
|
|
network_label_ = lv_label_create(top_bar_);
|
|
lv_label_set_text(network_label_, "");
|
|
lv_obj_set_style_text_font(network_label_, icon_font, 0);
|
|
lv_obj_set_style_text_color(network_label_, lvgl_theme->text_color(), 0);
|
|
|
|
// Right icons container
|
|
lv_obj_t* right_icons = lv_obj_create(top_bar_);
|
|
lv_obj_set_size(right_icons, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
|
lv_obj_set_style_bg_opa(right_icons, LV_OPA_TRANSP, 0);
|
|
lv_obj_set_style_border_width(right_icons, 0, 0);
|
|
lv_obj_set_style_pad_all(right_icons, 0, 0);
|
|
lv_obj_set_flex_flow(right_icons, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(right_icons, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
|
|
mute_label_ = lv_label_create(right_icons);
|
|
lv_label_set_text(mute_label_, "");
|
|
lv_obj_set_style_text_font(mute_label_, icon_font, 0);
|
|
lv_obj_set_style_text_color(mute_label_, lvgl_theme->text_color(), 0);
|
|
|
|
battery_label_ = lv_label_create(right_icons);
|
|
lv_label_set_text(battery_label_, "");
|
|
lv_obj_set_style_text_font(battery_label_, icon_font, 0);
|
|
lv_obj_set_style_text_color(battery_label_, lvgl_theme->text_color(), 0);
|
|
lv_obj_set_style_margin_left(battery_label_, lvgl_theme->spacing(2), 0);
|
|
|
|
/* Layer 2: Status bar - for center text labels */
|
|
status_bar_ = lv_obj_create(screen);
|
|
lv_obj_set_size(status_bar_, LV_HOR_RES, LV_SIZE_CONTENT);
|
|
lv_obj_set_style_radius(status_bar_, 0, 0);
|
|
lv_obj_set_style_bg_opa(status_bar_, LV_OPA_TRANSP, 0); // Transparent background
|
|
lv_obj_set_style_border_width(status_bar_, 0, 0);
|
|
lv_obj_set_style_pad_all(status_bar_, 0, 0);
|
|
lv_obj_set_style_pad_top(status_bar_, lvgl_theme->spacing(2), 0);
|
|
lv_obj_set_style_pad_bottom(status_bar_, lvgl_theme->spacing(2), 0);
|
|
lv_obj_set_scrollbar_mode(status_bar_, LV_SCROLLBAR_MODE_OFF);
|
|
lv_obj_set_style_layout(status_bar_, LV_LAYOUT_NONE, 0); // Use absolute positioning
|
|
lv_obj_align(status_bar_, LV_ALIGN_TOP_MID, 0, 0); // Overlap with top_bar_
|
|
|
|
notification_label_ = lv_label_create(status_bar_);
|
|
lv_obj_set_width(notification_label_, LV_HOR_RES * 0.8);
|
|
lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_style_text_color(notification_label_, lvgl_theme->text_color(), 0);
|
|
lv_label_set_text(notification_label_, "");
|
|
lv_obj_align(notification_label_, LV_ALIGN_CENTER, 0, 0);
|
|
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
status_label_ = lv_label_create(status_bar_);
|
|
lv_obj_set_width(status_label_, LV_HOR_RES * 0.8);
|
|
lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
|
|
lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_style_text_color(status_label_, lvgl_theme->text_color(), 0);
|
|
lv_label_set_text(status_label_, Lang::Strings::INITIALIZING);
|
|
lv_obj_align(status_label_, LV_ALIGN_CENTER, 0, 0);
|
|
|
|
/* Content - Chat area */
|
|
content_ = lv_obj_create(container_);
|
|
lv_obj_set_style_radius(content_, 0, 0);
|
|
lv_obj_set_width(content_, LV_HOR_RES);
|
|
lv_obj_set_flex_grow(content_, 1);
|
|
lv_obj_set_style_pad_all(content_, lvgl_theme->spacing(4), 0);
|
|
lv_obj_set_style_border_width(content_, 0, 0);
|
|
lv_obj_set_style_bg_color(content_, lvgl_theme->chat_background_color(), 0); // Background for chat area
|
|
|
|
// Enable scrolling for chat content
|
|
lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF);
|
|
lv_obj_set_scroll_dir(content_, LV_DIR_VER);
|
|
|
|
// Create a flex container for chat messages
|
|
lv_obj_set_flex_flow(content_, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_flex_align(content_, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
|
lv_obj_set_style_pad_row(content_, lvgl_theme->spacing(4), 0); // Space between messages
|
|
|
|
// We'll create chat messages dynamically in SetChatMessage
|
|
chat_message_label_ = nullptr;
|
|
|
|
low_battery_popup_ = lv_obj_create(screen);
|
|
lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF);
|
|
lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, text_font->line_height * 2);
|
|
lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, -lvgl_theme->spacing(4));
|
|
lv_obj_set_style_bg_color(low_battery_popup_, lvgl_theme->low_battery_color(), 0);
|
|
lv_obj_set_style_radius(low_battery_popup_, lvgl_theme->spacing(4), 0);
|
|
low_battery_label_ = lv_label_create(low_battery_popup_);
|
|
lv_label_set_text(low_battery_label_, Lang::Strings::BATTERY_NEED_CHARGE);
|
|
lv_obj_set_style_text_color(low_battery_label_, lv_color_white(), 0);
|
|
lv_obj_center(low_battery_label_);
|
|
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
emoji_image_ = lv_img_create(screen);
|
|
lv_obj_align(emoji_image_, LV_ALIGN_TOP_MID, 0, text_font->line_height + lvgl_theme->spacing(8));
|
|
|
|
// Display AI logo while booting
|
|
emoji_label_ = lv_label_create(screen);
|
|
lv_obj_center(emoji_label_);
|
|
lv_obj_set_style_text_font(emoji_label_, large_icon_font, 0);
|
|
lv_obj_set_style_text_color(emoji_label_, lvgl_theme->text_color(), 0);
|
|
lv_label_set_text(emoji_label_, FONT_AWESOME_MICROCHIP_AI);
|
|
}
|
|
#if CONFIG_IDF_TARGET_ESP32P4
|
|
#define MAX_MESSAGES 40
|
|
#else
|
|
#define MAX_MESSAGES 20
|
|
#endif
|
|
void LcdDisplay::SetChatMessage(const char* role, const char* content) {
|
|
DisplayLockGuard lock(this);
|
|
if (content_ == nullptr) {
|
|
return;
|
|
}
|
|
|
|
// Check if message count exceeds limit
|
|
uint32_t child_count = lv_obj_get_child_cnt(content_);
|
|
if (child_count >= MAX_MESSAGES) {
|
|
// Delete the oldest message (first child object)
|
|
lv_obj_t* first_child = lv_obj_get_child(content_, 0);
|
|
if (first_child != nullptr) {
|
|
lv_obj_del(first_child);
|
|
// Refresh child count after deletion
|
|
child_count = lv_obj_get_child_cnt(content_);
|
|
}
|
|
// Scroll to the last message immediately (get last_child after deletion)
|
|
if (child_count > 0) {
|
|
lv_obj_t* last_child = lv_obj_get_child(content_, child_count - 1);
|
|
if (last_child != nullptr && lv_obj_is_valid(last_child)) {
|
|
lv_obj_scroll_to_view_recursive(last_child, LV_ANIM_OFF);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collapse system messages (if it's a system message, check if the last message is also a system message)
|
|
if (strcmp(role, "system") == 0) {
|
|
// Refresh child count to get accurate count after potential deletion above
|
|
child_count = lv_obj_get_child_cnt(content_);
|
|
if (child_count > 0) {
|
|
// Get the last message container
|
|
lv_obj_t* last_container = lv_obj_get_child(content_, child_count - 1);
|
|
if (last_container != nullptr && lv_obj_is_valid(last_container) && lv_obj_get_child_cnt(last_container) > 0) {
|
|
// Get the bubble inside the container
|
|
lv_obj_t* last_bubble = lv_obj_get_child(last_container, 0);
|
|
if (last_bubble != nullptr && lv_obj_is_valid(last_bubble)) {
|
|
// Check if bubble type is system message
|
|
void* bubble_type_ptr = lv_obj_get_user_data(last_bubble);
|
|
if (bubble_type_ptr != nullptr && strcmp((const char*)bubble_type_ptr, "system") == 0) {
|
|
// If the last message is also a system message, delete it
|
|
lv_obj_del(last_container);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Hide the centered AI logo
|
|
lv_obj_add_flag(emoji_label_, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
// Avoid empty message boxes
|
|
if(strlen(content) == 0) {
|
|
return;
|
|
}
|
|
|
|
auto lvgl_theme = static_cast<LvglTheme*>(current_theme_);
|
|
auto text_font = lvgl_theme->text_font()->font();
|
|
|
|
// Create a message bubble
|
|
lv_obj_t* msg_bubble = lv_obj_create(content_);
|
|
lv_obj_set_style_radius(msg_bubble, 8, 0);
|
|
lv_obj_set_scrollbar_mode(msg_bubble, LV_SCROLLBAR_MODE_OFF);
|
|
lv_obj_set_style_border_width(msg_bubble, 0, 0);
|
|
lv_obj_set_style_pad_all(msg_bubble, lvgl_theme->spacing(4), 0);
|
|
|
|
// Create the message text
|
|
lv_obj_t* msg_text = lv_label_create(msg_bubble);
|
|
lv_label_set_text(msg_text, content);
|
|
|
|
// Calculate actual text width
|
|
lv_coord_t text_width = lv_txt_get_width(content, strlen(content), text_font, 0);
|
|
|
|
// Calculate bubble width
|
|
lv_coord_t max_width = LV_HOR_RES * 85 / 100 - 16; // 85% of screen width
|
|
lv_coord_t min_width = 20;
|
|
lv_coord_t bubble_width;
|
|
|
|
// Ensure text width is not less than minimum width
|
|
if (text_width < min_width) {
|
|
text_width = min_width;
|
|
}
|
|
|
|
// If text width is less than max width, use text width
|
|
if (text_width < max_width) {
|
|
bubble_width = text_width;
|
|
} else {
|
|
bubble_width = max_width;
|
|
}
|
|
|
|
// Set message text width
|
|
lv_obj_set_width(msg_text, bubble_width); // Subtract padding
|
|
lv_label_set_long_mode(msg_text, LV_LABEL_LONG_WRAP);
|
|
|
|
// Set bubble width
|
|
lv_obj_set_width(msg_bubble, bubble_width);
|
|
lv_obj_set_height(msg_bubble, LV_SIZE_CONTENT);
|
|
|
|
// Set alignment and style based on message role
|
|
if (strcmp(role, "user") == 0) {
|
|
// User messages are right-aligned with green background
|
|
lv_obj_set_style_bg_color(msg_bubble, lvgl_theme->user_bubble_color(), 0);
|
|
lv_obj_set_style_bg_opa(msg_bubble, LV_OPA_70, 0);
|
|
// Set text color for contrast
|
|
lv_obj_set_style_text_color(msg_text, lvgl_theme->text_color(), 0);
|
|
|
|
// Set custom attribute to mark bubble type
|
|
lv_obj_set_user_data(msg_bubble, (void*)"user");
|
|
|
|
// Set appropriate width for content
|
|
lv_obj_set_width(msg_bubble, LV_SIZE_CONTENT);
|
|
lv_obj_set_height(msg_bubble, LV_SIZE_CONTENT);
|
|
|
|
// Don't grow
|
|
lv_obj_set_style_flex_grow(msg_bubble, 0, 0);
|
|
} else if (strcmp(role, "assistant") == 0) {
|
|
// Assistant messages are left-aligned with white background
|
|
lv_obj_set_style_bg_color(msg_bubble, lvgl_theme->assistant_bubble_color(), 0);
|
|
lv_obj_set_style_bg_opa(msg_bubble, LV_OPA_70, 0);
|
|
// Set text color for contrast
|
|
lv_obj_set_style_text_color(msg_text, lvgl_theme->text_color(), 0);
|
|
|
|
// Set custom attribute to mark bubble type
|
|
lv_obj_set_user_data(msg_bubble, (void*)"assistant");
|
|
|
|
// Set appropriate width for content
|
|
lv_obj_set_width(msg_bubble, LV_SIZE_CONTENT);
|
|
lv_obj_set_height(msg_bubble, LV_SIZE_CONTENT);
|
|
|
|
// Don't grow
|
|
lv_obj_set_style_flex_grow(msg_bubble, 0, 0);
|
|
} else if (strcmp(role, "system") == 0) {
|
|
// System messages are center-aligned with light gray background
|
|
lv_obj_set_style_bg_color(msg_bubble, lvgl_theme->system_bubble_color(), 0);
|
|
lv_obj_set_style_bg_opa(msg_bubble, LV_OPA_70, 0);
|
|
// Set text color for contrast
|
|
lv_obj_set_style_text_color(msg_text, lvgl_theme->system_text_color(), 0);
|
|
|
|
// Set custom attribute to mark bubble type
|
|
lv_obj_set_user_data(msg_bubble, (void*)"system");
|
|
|
|
// Set appropriate width for content
|
|
lv_obj_set_width(msg_bubble, LV_SIZE_CONTENT);
|
|
lv_obj_set_height(msg_bubble, LV_SIZE_CONTENT);
|
|
|
|
// Don't grow
|
|
lv_obj_set_style_flex_grow(msg_bubble, 0, 0);
|
|
}
|
|
|
|
// Create a full-width container for user messages to ensure right alignment
|
|
if (strcmp(role, "user") == 0) {
|
|
// Create a full-width container
|
|
lv_obj_t* container = lv_obj_create(content_);
|
|
lv_obj_set_width(container, LV_HOR_RES);
|
|
lv_obj_set_height(container, LV_SIZE_CONTENT);
|
|
|
|
// Make container transparent and borderless
|
|
lv_obj_set_style_bg_opa(container, LV_OPA_TRANSP, 0);
|
|
lv_obj_set_style_border_width(container, 0, 0);
|
|
lv_obj_set_style_pad_all(container, 0, 0);
|
|
|
|
// Move the message bubble into this container
|
|
lv_obj_set_parent(msg_bubble, container);
|
|
|
|
// Right align the bubble in the container
|
|
lv_obj_align(msg_bubble, LV_ALIGN_RIGHT_MID, -25, 0);
|
|
|
|
// Auto-scroll to this container
|
|
lv_obj_scroll_to_view_recursive(container, LV_ANIM_ON);
|
|
} else if (strcmp(role, "system") == 0) {
|
|
// Create full-width container for system messages to ensure center alignment
|
|
lv_obj_t* container = lv_obj_create(content_);
|
|
lv_obj_set_width(container, LV_HOR_RES);
|
|
lv_obj_set_height(container, LV_SIZE_CONTENT);
|
|
|
|
lv_obj_set_style_bg_opa(container, LV_OPA_TRANSP, 0);
|
|
lv_obj_set_style_border_width(container, 0, 0);
|
|
lv_obj_set_style_pad_all(container, 0, 0);
|
|
|
|
lv_obj_set_parent(msg_bubble, container);
|
|
lv_obj_align(msg_bubble, LV_ALIGN_CENTER, 0, 0);
|
|
lv_obj_scroll_to_view_recursive(container, LV_ANIM_ON);
|
|
} else {
|
|
// For assistant messages
|
|
// Left align assistant messages
|
|
lv_obj_align(msg_bubble, LV_ALIGN_LEFT_MID, 0, 0);
|
|
|
|
// Auto-scroll to the message bubble
|
|
lv_obj_scroll_to_view_recursive(msg_bubble, LV_ANIM_ON);
|
|
}
|
|
|
|
// Store reference to the latest message label
|
|
chat_message_label_ = msg_text;
|
|
}
|
|
|
|
void LcdDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image) {
|
|
DisplayLockGuard lock(this);
|
|
if (content_ == nullptr) {
|
|
return;
|
|
}
|
|
|
|
if (image == nullptr) {
|
|
return;
|
|
}
|
|
|
|
auto lvgl_theme = static_cast<LvglTheme*>(current_theme_);
|
|
// Create a message bubble for image preview
|
|
lv_obj_t* img_bubble = lv_obj_create(content_);
|
|
lv_obj_set_style_radius(img_bubble, 8, 0);
|
|
lv_obj_set_scrollbar_mode(img_bubble, LV_SCROLLBAR_MODE_OFF);
|
|
lv_obj_set_style_border_width(img_bubble, 0, 0);
|
|
lv_obj_set_style_pad_all(img_bubble, lvgl_theme->spacing(4), 0);
|
|
|
|
// Set image bubble background color (similar to system message)
|
|
lv_obj_set_style_bg_color(img_bubble, lvgl_theme->assistant_bubble_color(), 0);
|
|
lv_obj_set_style_bg_opa(img_bubble, LV_OPA_70, 0);
|
|
|
|
// Set custom attribute to mark bubble type
|
|
lv_obj_set_user_data(img_bubble, (void*)"image");
|
|
|
|
// Create the image object inside the bubble
|
|
lv_obj_t* preview_image = lv_image_create(img_bubble);
|
|
|
|
// Calculate appropriate size for the image
|
|
lv_coord_t max_width = LV_HOR_RES * 70 / 100; // 70% of screen width
|
|
lv_coord_t max_height = LV_VER_RES * 50 / 100; // 50% of screen height
|
|
|
|
// Calculate zoom factor to fit within maximum dimensions
|
|
auto img_dsc = image->image_dsc();
|
|
lv_coord_t img_width = img_dsc->header.w;
|
|
lv_coord_t img_height = img_dsc->header.h;
|
|
if (img_width == 0 || img_height == 0) {
|
|
img_width = max_width;
|
|
img_height = max_height;
|
|
ESP_LOGW(TAG, "Invalid image dimensions: %ld x %ld, using default dimensions: %ld x %ld", img_width, img_height, max_width, max_height);
|
|
}
|
|
|
|
lv_coord_t zoom_w = (max_width * 256) / img_width;
|
|
lv_coord_t zoom_h = (max_height * 256) / img_height;
|
|
lv_coord_t zoom = (zoom_w < zoom_h) ? zoom_w : zoom_h;
|
|
|
|
// Ensure zoom doesn't exceed 256 (100%)
|
|
if (zoom > 256) zoom = 256;
|
|
|
|
// Set image properties
|
|
lv_image_set_src(preview_image, img_dsc);
|
|
lv_image_set_scale(preview_image, zoom);
|
|
|
|
// Add event handler to clean up LvglImage when image is deleted
|
|
// We need to transfer ownership of the unique_ptr to the event callback
|
|
LvglImage* raw_image = image.release(); // Release ownership of smart pointer
|
|
lv_obj_add_event_cb(preview_image, [](lv_event_t* e) {
|
|
LvglImage* img = (LvglImage*)lv_event_get_user_data(e);
|
|
if (img != nullptr) {
|
|
delete img; // Properly release memory by deleting LvglImage object
|
|
}
|
|
}, LV_EVENT_DELETE, (void*)raw_image);
|
|
|
|
// Calculate actual scaled image dimensions
|
|
lv_coord_t scaled_width = (img_width * zoom) / 256;
|
|
lv_coord_t scaled_height = (img_height * zoom) / 256;
|
|
|
|
// Set bubble size to be 16 pixels larger than the image (8 pixels on each side)
|
|
lv_obj_set_width(img_bubble, scaled_width + 16);
|
|
lv_obj_set_height(img_bubble, scaled_height + 16);
|
|
|
|
// Don't grow in flex layout
|
|
lv_obj_set_style_flex_grow(img_bubble, 0, 0);
|
|
|
|
// Center the image within the bubble
|
|
lv_obj_center(preview_image);
|
|
|
|
// Left align the image bubble like assistant messages
|
|
lv_obj_align(img_bubble, LV_ALIGN_LEFT_MID, 0, 0);
|
|
|
|
// Auto-scroll to the image bubble
|
|
lv_obj_scroll_to_view_recursive(img_bubble, LV_ANIM_ON);
|
|
}
|
|
#else
|
|
void LcdDisplay::SetupUI() {
|
|
DisplayLockGuard lock(this);
|
|
LvglTheme* lvgl_theme = static_cast<LvglTheme*>(current_theme_);
|
|
auto text_font = lvgl_theme->text_font()->font();
|
|
auto icon_font = lvgl_theme->icon_font()->font();
|
|
auto large_icon_font = lvgl_theme->large_icon_font()->font();
|
|
|
|
auto screen = lv_screen_active();
|
|
lv_obj_set_style_text_font(screen, text_font, 0);
|
|
lv_obj_set_style_text_color(screen, lvgl_theme->text_color(), 0);
|
|
lv_obj_set_style_bg_color(screen, lvgl_theme->background_color(), 0);
|
|
|
|
/* Container - used as background */
|
|
container_ = lv_obj_create(screen);
|
|
lv_obj_set_size(container_, LV_HOR_RES, LV_VER_RES);
|
|
lv_obj_set_style_radius(container_, 0, 0);
|
|
lv_obj_set_style_pad_all(container_, 0, 0);
|
|
lv_obj_set_style_border_width(container_, 0, 0);
|
|
lv_obj_set_style_bg_color(container_, lvgl_theme->background_color(), 0);
|
|
lv_obj_set_style_border_color(container_, lvgl_theme->border_color(), 0);
|
|
|
|
/* Bottom layer: emoji_box_ - centered display */
|
|
emoji_box_ = lv_obj_create(screen);
|
|
lv_obj_set_size(emoji_box_, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
|
lv_obj_set_style_bg_opa(emoji_box_, LV_OPA_TRANSP, 0);
|
|
lv_obj_set_style_pad_all(emoji_box_, 0, 0);
|
|
lv_obj_set_style_border_width(emoji_box_, 0, 0);
|
|
lv_obj_align(emoji_box_, LV_ALIGN_CENTER, 0, 0);
|
|
|
|
emoji_label_ = lv_label_create(emoji_box_);
|
|
lv_obj_set_style_text_font(emoji_label_, large_icon_font, 0);
|
|
lv_obj_set_style_text_color(emoji_label_, lvgl_theme->text_color(), 0);
|
|
lv_label_set_text(emoji_label_, FONT_AWESOME_MICROCHIP_AI);
|
|
|
|
emoji_image_ = lv_img_create(emoji_box_);
|
|
lv_obj_center(emoji_image_);
|
|
lv_obj_add_flag(emoji_image_, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
/* Middle layer: preview_image_ - centered display */
|
|
preview_image_ = lv_image_create(screen);
|
|
lv_obj_set_size(preview_image_, width_ / 2, height_ / 2);
|
|
lv_obj_align(preview_image_, LV_ALIGN_CENTER, 0, 0);
|
|
lv_obj_add_flag(preview_image_, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
/* Layer 1: Top bar - for status icons */
|
|
top_bar_ = lv_obj_create(screen);
|
|
lv_obj_set_size(top_bar_, LV_HOR_RES, LV_SIZE_CONTENT);
|
|
lv_obj_set_style_radius(top_bar_, 0, 0);
|
|
lv_obj_set_style_bg_opa(top_bar_, LV_OPA_50, 0); // 50% opacity background
|
|
lv_obj_set_style_bg_color(top_bar_, lvgl_theme->background_color(), 0);
|
|
lv_obj_set_style_border_width(top_bar_, 0, 0);
|
|
lv_obj_set_style_pad_all(top_bar_, 0, 0);
|
|
lv_obj_set_style_pad_top(top_bar_, lvgl_theme->spacing(2), 0);
|
|
lv_obj_set_style_pad_bottom(top_bar_, lvgl_theme->spacing(2), 0);
|
|
lv_obj_set_style_pad_left(top_bar_, lvgl_theme->spacing(4), 0);
|
|
lv_obj_set_style_pad_right(top_bar_, lvgl_theme->spacing(4), 0);
|
|
lv_obj_set_flex_flow(top_bar_, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(top_bar_, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_scrollbar_mode(top_bar_, LV_SCROLLBAR_MODE_OFF);
|
|
lv_obj_align(top_bar_, LV_ALIGN_TOP_MID, 0, 0);
|
|
|
|
// Left icon
|
|
network_label_ = lv_label_create(top_bar_);
|
|
lv_label_set_text(network_label_, "");
|
|
lv_obj_set_style_text_font(network_label_, icon_font, 0);
|
|
lv_obj_set_style_text_color(network_label_, lvgl_theme->text_color(), 0);
|
|
|
|
// Right icons container
|
|
lv_obj_t* right_icons = lv_obj_create(top_bar_);
|
|
lv_obj_set_size(right_icons, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
|
lv_obj_set_style_bg_opa(right_icons, LV_OPA_TRANSP, 0);
|
|
lv_obj_set_style_border_width(right_icons, 0, 0);
|
|
lv_obj_set_style_pad_all(right_icons, 0, 0);
|
|
lv_obj_set_flex_flow(right_icons, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_flex_align(right_icons, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
|
|
mute_label_ = lv_label_create(right_icons);
|
|
lv_label_set_text(mute_label_, "");
|
|
lv_obj_set_style_text_font(mute_label_, icon_font, 0);
|
|
lv_obj_set_style_text_color(mute_label_, lvgl_theme->text_color(), 0);
|
|
|
|
battery_label_ = lv_label_create(right_icons);
|
|
lv_label_set_text(battery_label_, "");
|
|
lv_obj_set_style_text_font(battery_label_, icon_font, 0);
|
|
lv_obj_set_style_text_color(battery_label_, lvgl_theme->text_color(), 0);
|
|
lv_obj_set_style_margin_left(battery_label_, lvgl_theme->spacing(2), 0);
|
|
|
|
/* Layer 2: Status bar - for center text labels */
|
|
status_bar_ = lv_obj_create(screen);
|
|
lv_obj_set_size(status_bar_, LV_HOR_RES, LV_SIZE_CONTENT);
|
|
lv_obj_set_style_radius(status_bar_, 0, 0);
|
|
lv_obj_set_style_bg_opa(status_bar_, LV_OPA_TRANSP, 0); // Transparent background
|
|
lv_obj_set_style_border_width(status_bar_, 0, 0);
|
|
lv_obj_set_style_pad_all(status_bar_, 0, 0);
|
|
lv_obj_set_style_pad_top(status_bar_, lvgl_theme->spacing(2), 0);
|
|
lv_obj_set_style_pad_bottom(status_bar_, lvgl_theme->spacing(2), 0);
|
|
lv_obj_set_scrollbar_mode(status_bar_, LV_SCROLLBAR_MODE_OFF);
|
|
lv_obj_set_style_layout(status_bar_, LV_LAYOUT_NONE, 0); // Use absolute positioning
|
|
lv_obj_align(status_bar_, LV_ALIGN_TOP_MID, 0, 0); // Overlap with top_bar_
|
|
|
|
notification_label_ = lv_label_create(status_bar_);
|
|
lv_obj_set_width(notification_label_, LV_HOR_RES * 0.75);
|
|
lv_obj_set_style_text_align(notification_label_, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_style_text_color(notification_label_, lvgl_theme->text_color(), 0);
|
|
lv_label_set_text(notification_label_, "");
|
|
lv_obj_align(notification_label_, LV_ALIGN_CENTER, 0, 0);
|
|
lv_obj_add_flag(notification_label_, LV_OBJ_FLAG_HIDDEN);
|
|
|
|
status_label_ = lv_label_create(status_bar_);
|
|
lv_obj_set_width(status_label_, LV_HOR_RES * 0.75);
|
|
lv_label_set_long_mode(status_label_, LV_LABEL_LONG_SCROLL_CIRCULAR);
|
|
lv_obj_set_style_text_align(status_label_, LV_TEXT_ALIGN_CENTER, 0);
|
|
lv_obj_set_style_text_color(status_label_, lvgl_theme->text_color(), 0);
|
|
lv_label_set_text(status_label_, Lang::Strings::INITIALIZING);
|
|
lv_obj_align(status_label_, LV_ALIGN_CENTER, 0, 0);
|
|
|
|
/* Top layer: Bottom bar - fixed at bottom, minimum height 48, height can be adaptive */
|
|
bottom_bar_ = lv_obj_create(screen);
|
|
lv_obj_set_width(bottom_bar_, LV_HOR_RES);
|
|
lv_obj_set_height(bottom_bar_, LV_SIZE_CONTENT);
|
|
lv_obj_set_style_min_height(bottom_bar_, 48, 0); // Set minimum height 48
|
|
lv_obj_set_style_radius(bottom_bar_, 0, 0);
|
|
lv_obj_set_style_bg_color(bottom_bar_, lvgl_theme->background_color(), 0);
|
|
lv_obj_set_style_text_color(bottom_bar_, lvgl_theme->text_color(), 0);
|
|
lv_obj_set_style_pad_top(bottom_bar_, lvgl_theme->spacing(2), 0);
|
|
lv_obj_set_style_pad_bottom(bottom_bar_, lvgl_theme->spacing(2), 0);
|
|
lv_obj_set_style_pad_left(bottom_bar_, lvgl_theme->spacing(4), 0);
|
|
lv_obj_set_style_pad_right(bottom_bar_, lvgl_theme->spacing(4), 0);
|
|
lv_obj_set_style_border_width(bottom_bar_, 0, 0);
|
|
lv_obj_align(bottom_bar_, LV_ALIGN_BOTTOM_MID, 0, 0);
|
|
|
|
/* chat_message_label_ placed in bottom_bar_ and vertically centered */
|
|
chat_message_label_ = lv_label_create(bottom_bar_);
|
|
lv_label_set_text(chat_message_label_, "");
|
|
lv_obj_set_width(chat_message_label_, LV_HOR_RES - lvgl_theme->spacing(8)); // Subtract left and right padding
|
|
lv_label_set_long_mode(chat_message_label_, LV_LABEL_LONG_WRAP); // Auto wrap mode
|
|
lv_obj_set_style_text_align(chat_message_label_, LV_TEXT_ALIGN_CENTER, 0); // Center text alignment
|
|
lv_obj_set_style_text_color(chat_message_label_, lvgl_theme->text_color(), 0);
|
|
lv_obj_align(chat_message_label_, LV_ALIGN_CENTER, 0, 0); // Vertically and horizontally centered in bottom_bar_
|
|
|
|
low_battery_popup_ = lv_obj_create(screen);
|
|
lv_obj_set_scrollbar_mode(low_battery_popup_, LV_SCROLLBAR_MODE_OFF);
|
|
lv_obj_set_size(low_battery_popup_, LV_HOR_RES * 0.9, text_font->line_height * 2);
|
|
lv_obj_align(low_battery_popup_, LV_ALIGN_BOTTOM_MID, 0, -lvgl_theme->spacing(4));
|
|
lv_obj_set_style_bg_color(low_battery_popup_, lvgl_theme->low_battery_color(), 0);
|
|
lv_obj_set_style_radius(low_battery_popup_, lvgl_theme->spacing(4), 0);
|
|
|
|
low_battery_label_ = lv_label_create(low_battery_popup_);
|
|
lv_label_set_text(low_battery_label_, Lang::Strings::BATTERY_NEED_CHARGE);
|
|
lv_obj_set_style_text_color(low_battery_label_, lv_color_white(), 0);
|
|
lv_obj_center(low_battery_label_);
|
|
lv_obj_add_flag(low_battery_popup_, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
void LcdDisplay::SetPreviewImage(std::unique_ptr<LvglImage> image) {
|
|
DisplayLockGuard lock(this);
|
|
if (preview_image_ == nullptr) {
|
|
ESP_LOGE(TAG, "Preview image is not initialized");
|
|
return;
|
|
}
|
|
|
|
if (image == nullptr) {
|
|
esp_timer_stop(preview_timer_);
|
|
lv_obj_remove_flag(emoji_box_, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_add_flag(preview_image_, LV_OBJ_FLAG_HIDDEN);
|
|
preview_image_cached_.reset();
|
|
if (gif_controller_) {
|
|
gif_controller_->Start();
|
|
}
|
|
return;
|
|
}
|
|
|
|
preview_image_cached_ = std::move(image);
|
|
auto img_dsc = preview_image_cached_->image_dsc();
|
|
lv_image_set_src(preview_image_, img_dsc);
|
|
if (img_dsc->header.w > 0 && img_dsc->header.h > 0) {
|
|
// zoom factor 0.5
|
|
lv_image_set_scale(preview_image_, 128 * width_ / img_dsc->header.w);
|
|
}
|
|
|
|
// Hide emoji_box_
|
|
if (gif_controller_) {
|
|
gif_controller_->Stop();
|
|
}
|
|
lv_obj_add_flag(emoji_box_, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_remove_flag(preview_image_, LV_OBJ_FLAG_HIDDEN);
|
|
esp_timer_stop(preview_timer_);
|
|
ESP_ERROR_CHECK(esp_timer_start_once(preview_timer_, PREVIEW_IMAGE_DURATION_MS * 1000));
|
|
}
|
|
|
|
void LcdDisplay::SetChatMessage(const char* role, const char* content) {
|
|
DisplayLockGuard lock(this);
|
|
if (chat_message_label_ == nullptr) {
|
|
return;
|
|
}
|
|
lv_label_set_text(chat_message_label_, content);
|
|
}
|
|
#endif
|
|
|
|
void LcdDisplay::SetEmotion(const char* emotion) {
|
|
// Stop any running GIF animation
|
|
if (gif_controller_) {
|
|
DisplayLockGuard lock(this);
|
|
gif_controller_->Stop();
|
|
gif_controller_.reset();
|
|
}
|
|
|
|
if (emoji_image_ == nullptr) {
|
|
return;
|
|
}
|
|
|
|
auto emoji_collection = static_cast<LvglTheme*>(current_theme_)->emoji_collection();
|
|
auto image = emoji_collection != nullptr ? emoji_collection->GetEmojiImage(emotion) : nullptr;
|
|
if (image == nullptr) {
|
|
const char* utf8 = font_awesome_get_utf8(emotion);
|
|
if (utf8 != nullptr && emoji_label_ != nullptr) {
|
|
DisplayLockGuard lock(this);
|
|
lv_label_set_text(emoji_label_, utf8);
|
|
lv_obj_add_flag(emoji_image_, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_remove_flag(emoji_label_, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
return;
|
|
}
|
|
|
|
DisplayLockGuard lock(this);
|
|
if (image->IsGif()) {
|
|
// Create new GIF controller
|
|
gif_controller_ = std::make_unique<LvglGif>(image->image_dsc());
|
|
|
|
if (gif_controller_->IsLoaded()) {
|
|
// Set up frame update callback
|
|
gif_controller_->SetFrameCallback([this]() {
|
|
lv_image_set_src(emoji_image_, gif_controller_->image_dsc());
|
|
});
|
|
|
|
// Set initial frame and start animation
|
|
lv_image_set_src(emoji_image_, gif_controller_->image_dsc());
|
|
gif_controller_->Start();
|
|
|
|
// Show GIF, hide others
|
|
lv_obj_add_flag(emoji_label_, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_remove_flag(emoji_image_, LV_OBJ_FLAG_HIDDEN);
|
|
} else {
|
|
ESP_LOGE(TAG, "Failed to load GIF for emotion: %s", emotion);
|
|
gif_controller_.reset();
|
|
}
|
|
} else {
|
|
lv_image_set_src(emoji_image_, image->image_dsc());
|
|
lv_obj_add_flag(emoji_label_, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_remove_flag(emoji_image_, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
|
|
#if CONFIG_USE_WECHAT_MESSAGE_STYLE
|
|
// In WeChat message style, if emotion is neutral, don't display it
|
|
uint32_t child_count = lv_obj_get_child_cnt(content_);
|
|
if (strcmp(emotion, "neutral") == 0 && child_count > 0) {
|
|
// Stop GIF animation if running
|
|
if (gif_controller_) {
|
|
gif_controller_->Stop();
|
|
gif_controller_.reset();
|
|
}
|
|
|
|
lv_obj_add_flag(emoji_image_, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_add_flag(emoji_label_, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void LcdDisplay::SetTheme(Theme* theme) {
|
|
DisplayLockGuard lock(this);
|
|
|
|
auto lvgl_theme = static_cast<LvglTheme*>(theme);
|
|
|
|
// Get the active screen
|
|
lv_obj_t* screen = lv_screen_active();
|
|
|
|
// Set font
|
|
auto text_font = lvgl_theme->text_font()->font();
|
|
auto icon_font = lvgl_theme->icon_font()->font();
|
|
auto large_icon_font = lvgl_theme->large_icon_font()->font();
|
|
|
|
if (text_font->line_height >= 40) {
|
|
lv_obj_set_style_text_font(mute_label_, large_icon_font, 0);
|
|
lv_obj_set_style_text_font(battery_label_, large_icon_font, 0);
|
|
lv_obj_set_style_text_font(network_label_, large_icon_font, 0);
|
|
} else {
|
|
lv_obj_set_style_text_font(mute_label_, icon_font, 0);
|
|
lv_obj_set_style_text_font(battery_label_, icon_font, 0);
|
|
lv_obj_set_style_text_font(network_label_, icon_font, 0);
|
|
}
|
|
|
|
// Set parent text color
|
|
lv_obj_set_style_text_font(screen, text_font, 0);
|
|
lv_obj_set_style_text_color(screen, lvgl_theme->text_color(), 0);
|
|
|
|
// Set background image
|
|
if (lvgl_theme->background_image() != nullptr) {
|
|
lv_obj_set_style_bg_image_src(container_, lvgl_theme->background_image()->image_dsc(), 0);
|
|
} else {
|
|
lv_obj_set_style_bg_image_src(container_, nullptr, 0);
|
|
lv_obj_set_style_bg_color(container_, lvgl_theme->background_color(), 0);
|
|
}
|
|
|
|
// Update top bar background color with 50% opacity
|
|
if (top_bar_ != nullptr) {
|
|
lv_obj_set_style_bg_opa(top_bar_, LV_OPA_50, 0);
|
|
lv_obj_set_style_bg_color(top_bar_, lvgl_theme->background_color(), 0);
|
|
}
|
|
|
|
// Update status bar elements
|
|
lv_obj_set_style_text_color(network_label_, lvgl_theme->text_color(), 0);
|
|
lv_obj_set_style_text_color(status_label_, lvgl_theme->text_color(), 0);
|
|
lv_obj_set_style_text_color(notification_label_, lvgl_theme->text_color(), 0);
|
|
lv_obj_set_style_text_color(mute_label_, lvgl_theme->text_color(), 0);
|
|
lv_obj_set_style_text_color(battery_label_, lvgl_theme->text_color(), 0);
|
|
lv_obj_set_style_text_color(emoji_label_, lvgl_theme->text_color(), 0);
|
|
|
|
// If we have the chat message style, update all message bubbles
|
|
#if CONFIG_USE_WECHAT_MESSAGE_STYLE
|
|
// Set content background opacity
|
|
lv_obj_set_style_bg_opa(content_, LV_OPA_TRANSP, 0);
|
|
|
|
// Iterate through all children of content (message containers or bubbles)
|
|
uint32_t child_count = lv_obj_get_child_cnt(content_);
|
|
for (uint32_t i = 0; i < child_count; i++) {
|
|
lv_obj_t* obj = lv_obj_get_child(content_, i);
|
|
if (obj == nullptr) continue;
|
|
|
|
lv_obj_t* bubble = nullptr;
|
|
|
|
// Check if this object is a container or bubble
|
|
// If it's a container (user or system message), get its child as bubble
|
|
// If it's a bubble (assistant message), use it directly
|
|
if (lv_obj_get_child_cnt(obj) > 0) {
|
|
// Might be a container, check if it's a user or system message container
|
|
// User and system message containers are transparent
|
|
lv_opa_t bg_opa = lv_obj_get_style_bg_opa(obj, 0);
|
|
if (bg_opa == LV_OPA_TRANSP) {
|
|
// This is a user or system message container
|
|
bubble = lv_obj_get_child(obj, 0);
|
|
} else {
|
|
// This might be an assistant message bubble itself
|
|
bubble = obj;
|
|
}
|
|
} else {
|
|
// No child elements, might be other UI elements, skip
|
|
continue;
|
|
}
|
|
|
|
if (bubble == nullptr) continue;
|
|
|
|
// Use saved user data to identify bubble type
|
|
void* bubble_type_ptr = lv_obj_get_user_data(bubble);
|
|
if (bubble_type_ptr != nullptr) {
|
|
const char* bubble_type = static_cast<const char*>(bubble_type_ptr);
|
|
|
|
// Apply correct color based on bubble type
|
|
if (strcmp(bubble_type, "user") == 0) {
|
|
lv_obj_set_style_bg_color(bubble, lvgl_theme->user_bubble_color(), 0);
|
|
} else if (strcmp(bubble_type, "assistant") == 0) {
|
|
lv_obj_set_style_bg_color(bubble, lvgl_theme->assistant_bubble_color(), 0);
|
|
} else if (strcmp(bubble_type, "system") == 0) {
|
|
lv_obj_set_style_bg_color(bubble, lvgl_theme->system_bubble_color(), 0);
|
|
} else if (strcmp(bubble_type, "image") == 0) {
|
|
lv_obj_set_style_bg_color(bubble, lvgl_theme->system_bubble_color(), 0);
|
|
}
|
|
|
|
// Update border color
|
|
lv_obj_set_style_border_color(bubble, lvgl_theme->border_color(), 0);
|
|
|
|
// Update text color for the message
|
|
if (lv_obj_get_child_cnt(bubble) > 0) {
|
|
lv_obj_t* text = lv_obj_get_child(bubble, 0);
|
|
if (text != nullptr) {
|
|
// Set text color based on bubble type
|
|
if (strcmp(bubble_type, "system") == 0) {
|
|
lv_obj_set_style_text_color(text, lvgl_theme->system_text_color(), 0);
|
|
} else {
|
|
lv_obj_set_style_text_color(text, lvgl_theme->text_color(), 0);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
ESP_LOGW(TAG, "child[%lu] Bubble type is not found", i);
|
|
}
|
|
}
|
|
#else
|
|
// Simple UI mode - just update the main chat message
|
|
if (chat_message_label_ != nullptr) {
|
|
lv_obj_set_style_text_color(chat_message_label_, lvgl_theme->text_color(), 0);
|
|
}
|
|
|
|
if (emoji_label_ != nullptr) {
|
|
lv_obj_set_style_text_color(emoji_label_, lvgl_theme->text_color(), 0);
|
|
}
|
|
|
|
// Update bottom bar background color with 50% opacity
|
|
if (bottom_bar_ != nullptr) {
|
|
lv_obj_set_style_bg_opa(bottom_bar_, LV_OPA_50, 0);
|
|
lv_obj_set_style_bg_color(bottom_bar_, lvgl_theme->background_color(), 0);
|
|
}
|
|
#endif
|
|
|
|
// Update low battery popup
|
|
lv_obj_set_style_bg_color(low_battery_popup_, lvgl_theme->low_battery_color(), 0);
|
|
|
|
// No errors occurred. Save theme to settings
|
|
Display::SetTheme(lvgl_theme);
|
|
}
|
|
|
|
void LcdDisplay::SetHideSubtitle(bool hide) {
|
|
DisplayLockGuard lock(this);
|
|
hide_subtitle_ = hide;
|
|
|
|
// Immediately update UI visibility based on the setting
|
|
if (bottom_bar_ != nullptr) {
|
|
if (hide) {
|
|
lv_obj_add_flag(bottom_bar_, LV_OBJ_FLAG_HIDDEN);
|
|
} else {
|
|
lv_obj_remove_flag(bottom_bar_, LV_OBJ_FLAG_HIDDEN);
|
|
}
|
|
}
|
|
}
|