* Enhance memory management in asset download and OTA processes by replacing static buffer allocations with dynamic memory allocation using heap capabilities. Update SPIRAM configuration values for improved memory usage. Add logging for error handling in buffer allocation failures. Introduce a new parameter in CloseAudioChannel to control goodbye message sending in MQTT and WebSocket protocols. * Update component versions in idf_component.yml and refactor GIF decoder functions for improved performance. Bump versions for audio effects, audio codec, LED strip, and other dependencies. Change GIF read and seek functions to inline for optimization. * Update language files to include new phrases for flight mode and connection status across multiple locales. Added translations for "FLIGHT_MODE_ON", "FLIGHT_MODE_OFF", "CONNECTION_SUCCESSFUL", and "MODEM_INIT_ERROR" in various languages, enhancing user experience and localization support. * fix wechat display
1201 lines
48 KiB
C++
1201 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 <src/misc/cache/lv_cache.h>
|
|
|
|
#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 bubble width constraints
|
|
lv_coord_t max_width = LV_HOR_RES * 85 / 100 - 16; // 85% of screen width
|
|
lv_coord_t min_width = 20;
|
|
|
|
// Let LVGL calculate the natural text width first
|
|
lv_obj_set_width(msg_text, LV_SIZE_CONTENT);
|
|
lv_obj_update_layout(msg_text);
|
|
lv_coord_t text_width = lv_obj_get_width(msg_text);
|
|
|
|
// Ensure text width is not less than minimum width
|
|
if (text_width < min_width) {
|
|
text_width = min_width;
|
|
}
|
|
|
|
// Constrain to max width
|
|
lv_coord_t bubble_width = (text_width < max_width) ? text_width : max_width;
|
|
|
|
// Set message text width
|
|
lv_obj_set_width(msg_text, bubble_width);
|
|
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, LV_PART_MAIN);
|
|
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);
|
|
}
|
|
}
|
|
}
|