285 lines
10 KiB
C++
285 lines
10 KiB
C++
#include "ButtonWidget.hpp"
|
|
#include "../WidgetManager.hpp"
|
|
#include "../Fonts.hpp"
|
|
#include "esp_log.h"
|
|
#include <cstdio>
|
|
|
|
static const char* TAG = "ButtonWidget";
|
|
|
|
static lv_text_align_t toLvTextAlign(uint8_t align) {
|
|
if (align == static_cast<uint8_t>(TextAlign::LEFT)) return LV_TEXT_ALIGN_LEFT;
|
|
if (align == static_cast<uint8_t>(TextAlign::RIGHT)) return LV_TEXT_ALIGN_RIGHT;
|
|
return LV_TEXT_ALIGN_CENTER;
|
|
}
|
|
|
|
static lv_flex_align_t toFlexAlign(uint8_t align) {
|
|
if (align == static_cast<uint8_t>(TextAlign::LEFT)) return LV_FLEX_ALIGN_START;
|
|
if (align == static_cast<uint8_t>(TextAlign::RIGHT)) return LV_FLEX_ALIGN_END;
|
|
return LV_FLEX_ALIGN_CENTER;
|
|
}
|
|
|
|
static void set_obj_name(lv_obj_t* obj, const char* base, uint8_t id, const char* suffix) {
|
|
#if LV_USE_OBJ_NAME
|
|
if (!obj || !base) return;
|
|
char name[48];
|
|
if (suffix && suffix[0] != '\0') {
|
|
snprintf(name, sizeof(name), "%s#%u_%s", base, id, suffix);
|
|
} else {
|
|
snprintf(name, sizeof(name), "%s#%u", base, id);
|
|
}
|
|
lv_obj_set_name(obj, name);
|
|
#else
|
|
(void)obj;
|
|
(void)base;
|
|
(void)id;
|
|
(void)suffix;
|
|
#endif
|
|
}
|
|
|
|
ButtonWidget::ButtonWidget(const WidgetConfig& config)
|
|
: Widget(config)
|
|
, contentContainer_(nullptr)
|
|
, label_(nullptr)
|
|
, iconLabel_(nullptr)
|
|
, shadowObj_(nullptr)
|
|
{
|
|
}
|
|
|
|
ButtonWidget::~ButtonWidget() {
|
|
// Remove event callback BEFORE the base class destructor deletes the object
|
|
if (obj_) {
|
|
lv_obj_remove_event_cb(obj_, clickCallback);
|
|
}
|
|
// Delete fake shadow (not a child of obj_, so must delete separately)
|
|
if (shadowObj_ && lv_obj_is_valid(shadowObj_)) {
|
|
lv_obj_delete(shadowObj_);
|
|
}
|
|
shadowObj_ = nullptr;
|
|
contentContainer_ = nullptr;
|
|
label_ = nullptr;
|
|
iconLabel_ = nullptr;
|
|
}
|
|
|
|
int ButtonWidget::encodeUtf8(uint32_t codepoint, char* buf) {
|
|
if (codepoint < 0x80) {
|
|
buf[0] = static_cast<char>(codepoint);
|
|
buf[1] = '\0';
|
|
return 1;
|
|
} else if (codepoint < 0x800) {
|
|
buf[0] = static_cast<char>(0xC0 | (codepoint >> 6));
|
|
buf[1] = static_cast<char>(0x80 | (codepoint & 0x3F));
|
|
buf[2] = '\0';
|
|
return 2;
|
|
} else if (codepoint < 0x10000) {
|
|
buf[0] = static_cast<char>(0xE0 | (codepoint >> 12));
|
|
buf[1] = static_cast<char>(0x80 | ((codepoint >> 6) & 0x3F));
|
|
buf[2] = static_cast<char>(0x80 | (codepoint & 0x3F));
|
|
buf[3] = '\0';
|
|
return 3;
|
|
} else if (codepoint < 0x110000) {
|
|
buf[0] = static_cast<char>(0xF0 | (codepoint >> 18));
|
|
buf[1] = static_cast<char>(0x80 | ((codepoint >> 12) & 0x3F));
|
|
buf[2] = static_cast<char>(0x80 | ((codepoint >> 6) & 0x3F));
|
|
buf[3] = static_cast<char>(0x80 | (codepoint & 0x3F));
|
|
buf[4] = '\0';
|
|
return 4;
|
|
}
|
|
buf[0] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
void ButtonWidget::clickCallback(lv_event_t* e) {
|
|
printf("ButtonWidget::clickCallback\n");
|
|
fflush(stdout);
|
|
ButtonWidget* widget = static_cast<ButtonWidget*>(lv_event_get_user_data(e));
|
|
if (!widget) {
|
|
printf("ButtonWidget: Widget is null!\n");
|
|
fflush(stdout);
|
|
return;
|
|
}
|
|
lv_obj_t* target = static_cast<lv_obj_t*>(lv_event_get_target(e));
|
|
WidgetManager::instance().handleButtonAction(widget->getConfig(), target);
|
|
}
|
|
|
|
void ButtonWidget::setupFlexLayout() {
|
|
if (contentContainer_ == nullptr) return;
|
|
|
|
// Determine flex direction based on icon position
|
|
bool isVertical = (config_.iconPosition == static_cast<uint8_t>(IconPosition::TOP) ||
|
|
config_.iconPosition == static_cast<uint8_t>(IconPosition::BOTTOM));
|
|
|
|
lv_obj_set_flex_flow(contentContainer_,
|
|
isVertical ? LV_FLEX_FLOW_COLUMN : LV_FLEX_FLOW_ROW);
|
|
lv_flex_align_t mainAlign = LV_FLEX_ALIGN_CENTER;
|
|
lv_flex_align_t crossAlign = LV_FLEX_ALIGN_CENTER;
|
|
lv_flex_align_t contentAlign = toFlexAlign(config_.textAlign);
|
|
if (contentAlign != LV_FLEX_ALIGN_CENTER) {
|
|
if (isVertical) {
|
|
crossAlign = contentAlign;
|
|
} else {
|
|
mainAlign = contentAlign;
|
|
}
|
|
}
|
|
lv_obj_set_flex_align(contentContainer_, mainAlign, crossAlign, LV_FLEX_ALIGN_CENTER);
|
|
|
|
// Set gap between icon and text
|
|
int gap = config_.iconGap > 0 ? config_.iconGap : 8;
|
|
lv_obj_set_style_pad_gap(contentContainer_, gap, 0);
|
|
}
|
|
|
|
void ButtonWidget::applyTextAlignment() {
|
|
if (label_ == nullptr) return;
|
|
lv_obj_set_style_text_align(label_, toLvTextAlign(config_.textAlign), 0);
|
|
lv_obj_center(label_);
|
|
}
|
|
|
|
lv_obj_t* ButtonWidget::create(lv_obj_t* parent) {
|
|
// Create fake shadow FIRST (so it's behind the button in z-order)
|
|
if (config_.shadow.enabled) {
|
|
createFakeShadow(parent);
|
|
}
|
|
|
|
obj_ = lv_btn_create(parent);
|
|
lv_obj_set_pos(obj_, config_.x, config_.y);
|
|
lv_obj_set_size(obj_, config_.width > 0 ? config_.width : 100,
|
|
config_.height > 0 ? config_.height : 50);
|
|
set_obj_name(obj_, "Button", config_.id, nullptr);
|
|
|
|
lv_obj_add_event_cb(obj_, clickCallback, LV_EVENT_CLICKED, this);
|
|
|
|
if (config_.isContainer) {
|
|
ESP_LOGI(TAG, "Created container button %d", config_.id);
|
|
return obj_;
|
|
}
|
|
|
|
bool hasIcon = config_.iconCodepoint > 0 && Fonts::hasIconFont();
|
|
|
|
if (hasIcon) {
|
|
// Create container for flex layout
|
|
contentContainer_ = lv_obj_create(obj_);
|
|
if (contentContainer_) {
|
|
set_obj_name(contentContainer_, "Button", config_.id, "content");
|
|
lv_obj_remove_style_all(contentContainer_);
|
|
lv_obj_set_size(contentContainer_, LV_PCT(100), LV_PCT(100));
|
|
lv_obj_center(contentContainer_);
|
|
lv_obj_clear_flag(contentContainer_, LV_OBJ_FLAG_CLICKABLE);
|
|
|
|
bool iconFirst = (config_.iconPosition == static_cast<uint8_t>(IconPosition::LEFT) ||
|
|
config_.iconPosition == static_cast<uint8_t>(IconPosition::TOP));
|
|
|
|
if (iconFirst) {
|
|
iconLabel_ = lv_label_create(contentContainer_);
|
|
set_obj_name(iconLabel_, "Button", config_.id, "icon");
|
|
char iconText[5];
|
|
encodeUtf8(config_.iconCodepoint, iconText);
|
|
lv_label_set_text(iconLabel_, iconText);
|
|
lv_obj_clear_flag(iconLabel_, LV_OBJ_FLAG_CLICKABLE);
|
|
}
|
|
|
|
label_ = lv_label_create(contentContainer_);
|
|
set_obj_name(label_, "Button", config_.id, "text");
|
|
lv_label_set_text(label_, config_.text);
|
|
lv_obj_clear_flag(label_, LV_OBJ_FLAG_CLICKABLE);
|
|
|
|
if (!iconFirst) {
|
|
iconLabel_ = lv_label_create(contentContainer_);
|
|
set_obj_name(iconLabel_, "Button", config_.id, "icon");
|
|
char iconText[5];
|
|
encodeUtf8(config_.iconCodepoint, iconText);
|
|
lv_label_set_text(iconLabel_, iconText);
|
|
lv_obj_clear_flag(iconLabel_, LV_OBJ_FLAG_CLICKABLE);
|
|
}
|
|
|
|
setupFlexLayout();
|
|
}
|
|
} else {
|
|
// Simple button without icon
|
|
label_ = lv_label_create(obj_);
|
|
set_obj_name(label_, "Button", config_.id, "text");
|
|
lv_label_set_text(label_, config_.text);
|
|
lv_label_set_long_mode(label_, LV_LABEL_LONG_WRAP);
|
|
lv_obj_set_width(label_, LV_SIZE_CONTENT);
|
|
applyTextAlignment();
|
|
lv_obj_clear_flag(label_, LV_OBJ_FLAG_CLICKABLE);
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Created button '%s' at %d,%d (icon: 0x%lx)",
|
|
config_.text, config_.x, config_.y, (unsigned long)config_.iconCodepoint);
|
|
return obj_;
|
|
}
|
|
|
|
void ButtonWidget::applyStyle() {
|
|
if (obj_ == nullptr) return;
|
|
|
|
// Apply common style to button (shadows skipped for buttons in Widget::applyShadowStyle)
|
|
applyCommonStyle();
|
|
|
|
// Apply fake shadow style
|
|
applyFakeShadowStyle();
|
|
|
|
// Apply text style to label
|
|
if (label_ != nullptr) {
|
|
lv_obj_set_style_text_color(label_, lv_color_make(
|
|
config_.textColor.r, config_.textColor.g, config_.textColor.b), 0);
|
|
lv_obj_set_style_text_font(label_, getFontBySize(config_.fontSize), 0);
|
|
}
|
|
|
|
// Apply icon style
|
|
if (iconLabel_ != nullptr) {
|
|
lv_obj_set_style_text_color(iconLabel_, lv_color_make(
|
|
config_.textColor.r, config_.textColor.g, config_.textColor.b), 0);
|
|
|
|
uint8_t sizeIdx = config_.iconSize < 6 ? config_.iconSize : config_.fontSize;
|
|
lv_obj_set_style_text_font(iconLabel_, Fonts::iconFont(sizeIdx), 0);
|
|
}
|
|
}
|
|
|
|
bool ButtonWidget::isChecked() const {
|
|
if (obj_ == nullptr) return false;
|
|
return (lv_obj_get_state(obj_) & LV_STATE_CHECKED) != 0;
|
|
}
|
|
|
|
void ButtonWidget::createFakeShadow(lv_obj_t* parent) {
|
|
// Create a simple rectangle as fake shadow (no LVGL shadow rendering)
|
|
shadowObj_ = lv_obj_create(parent);
|
|
if (!shadowObj_) return;
|
|
|
|
lv_obj_remove_style_all(shadowObj_);
|
|
lv_obj_clear_flag(shadowObj_, static_cast<lv_obj_flag_t>(LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_SCROLLABLE));
|
|
|
|
// Position: offset from button position by shadow offset
|
|
int16_t shadowX = config_.x + config_.shadow.offsetX;
|
|
int16_t shadowY = config_.y + config_.shadow.offsetY;
|
|
|
|
// Size: slightly larger than button (spread effect)
|
|
int16_t spread = config_.shadow.spread > 8 ? 8 : config_.shadow.spread;
|
|
int16_t shadowW = (config_.width > 0 ? config_.width : 100) + spread * 2;
|
|
int16_t shadowH = (config_.height > 0 ? config_.height : 50) + spread * 2;
|
|
|
|
// Adjust position for spread
|
|
shadowX -= spread;
|
|
shadowY -= spread;
|
|
|
|
lv_obj_set_pos(shadowObj_, shadowX, shadowY);
|
|
lv_obj_set_size(shadowObj_, shadowW, shadowH);
|
|
}
|
|
|
|
void ButtonWidget::applyFakeShadowStyle() {
|
|
if (!shadowObj_) return;
|
|
|
|
lv_color_t shadowColor = lv_color_make(
|
|
config_.shadow.color.r, config_.shadow.color.g, config_.shadow.color.b);
|
|
|
|
// Blur simulation: use lower opacity for softer look
|
|
uint8_t blur = config_.shadow.blur > 15 ? 15 : config_.shadow.blur;
|
|
uint8_t opa = blur > 0 ? (uint8_t)(120 - blur * 4) : 120; // Less blur = more solid
|
|
|
|
lv_obj_set_style_bg_color(shadowObj_, shadowColor, 0);
|
|
lv_obj_set_style_bg_opa(shadowObj_, opa, 0);
|
|
|
|
// Match button's border radius
|
|
if (config_.borderRadius > 0) {
|
|
lv_obj_set_style_radius(shadowObj_, config_.borderRadius + config_.shadow.spread, 0);
|
|
}
|
|
}
|