#include "ButtonWidget.hpp" #include "../WidgetManager.hpp" #include "../Fonts.hpp" #include "esp_log.h" #include static const char* TAG = "ButtonWidget"; static lv_text_align_t toLvTextAlign(uint8_t align) { if (align == static_cast(TextAlign::LEFT)) return LV_TEXT_ALIGN_LEFT; if (align == static_cast(TextAlign::RIGHT)) return LV_TEXT_ALIGN_RIGHT; return LV_TEXT_ALIGN_CENTER; } static lv_align_t toLvAlign(uint8_t align) { if (align == static_cast(TextAlign::LEFT)) return LV_ALIGN_LEFT_MID; if (align == static_cast(TextAlign::RIGHT)) return LV_ALIGN_RIGHT_MID; return LV_ALIGN_CENTER; } static lv_flex_align_t toFlexAlign(uint8_t align) { if (align == static_cast(TextAlign::LEFT)) return LV_FLEX_ALIGN_START; if (align == static_cast(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(codepoint); buf[1] = '\0'; return 1; } else if (codepoint < 0x800) { buf[0] = static_cast(0xC0 | (codepoint >> 6)); buf[1] = static_cast(0x80 | (codepoint & 0x3F)); buf[2] = '\0'; return 2; } else if (codepoint < 0x10000) { buf[0] = static_cast(0xE0 | (codepoint >> 12)); buf[1] = static_cast(0x80 | ((codepoint >> 6) & 0x3F)); buf[2] = static_cast(0x80 | (codepoint & 0x3F)); buf[3] = '\0'; return 3; } else if (codepoint < 0x110000) { buf[0] = static_cast(0xF0 | (codepoint >> 18)); buf[1] = static_cast(0x80 | ((codepoint >> 12) & 0x3F)); buf[2] = static_cast(0x80 | ((codepoint >> 6) & 0x3F)); buf[3] = static_cast(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(lv_event_get_user_data(e)); if (!widget) { printf("ButtonWidget: Widget is null!\n"); fflush(stdout); return; } lv_obj_t* target = static_cast(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(IconPosition::TOP) || config_.iconPosition == static_cast(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_align(label_, toLvAlign(config_.textAlign), 0, 0); } 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(IconPosition::LEFT) || config_.iconPosition == static_cast(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); // Icon fonts support sizes 0-13 (14px to 260px) uint8_t sizeIdx = config_.iconSize; if (sizeIdx > 13) sizeIdx = 13; lv_obj_set_style_text_font(iconLabel_, Fonts::iconFont(sizeIdx), 0); } } void ButtonWidget::onKnxSwitch(bool value) { cachedPrimaryValue_ = value ? 1.0f : 0.0f; hasCachedPrimary_ = true; if (obj_ && config_.isToggle) { if (value) { lv_obj_add_state(obj_, LV_STATE_CHECKED); } else { lv_obj_clear_state(obj_, LV_STATE_CHECKED); } } evaluateConditions(cachedPrimaryValue_, cachedSecondaryValue_, cachedTertiaryValue_); } bool ButtonWidget::evaluateConditions(float primaryValue, float secondaryValue, float tertiaryValue) { if (config_.conditionCount == 0) return false; const StyleCondition* bestMatch = nullptr; uint8_t bestPriority = 255; for (uint8_t i = 0; i < config_.conditionCount && i < MAX_CONDITIONS; ++i) { const StyleCondition& cond = config_.conditions[i]; if (!cond.enabled) continue; float checkValue = 0.0f; bool hasValue = false; switch (cond.source) { case ConditionSource::PRIMARY: checkValue = primaryValue; hasValue = hasCachedPrimary_; break; case ConditionSource::SECONDARY: checkValue = secondaryValue; hasValue = hasCachedSecondary_; break; case ConditionSource::TERTIARY: checkValue = tertiaryValue; hasValue = hasCachedTertiary_; break; } if (!hasValue) continue; bool matches = false; switch (cond.op) { case ConditionOp::LESS: matches = checkValue < cond.threshold; break; case ConditionOp::LESS_EQUAL: matches = checkValue <= cond.threshold; break; case ConditionOp::EQUAL: matches = checkValue == cond.threshold; break; case ConditionOp::GREATER_EQUAL: matches = checkValue >= cond.threshold; break; case ConditionOp::GREATER: matches = checkValue > cond.threshold; break; case ConditionOp::NOT_EQUAL: matches = checkValue != cond.threshold; break; } if (matches && cond.priority < bestPriority) { bestMatch = &cond; bestPriority = cond.priority; } } if (!bestMatch) { return false; } if (bestMatch->style.iconCodepoint != 0 && bestMatch->style.iconCodepoint != currentConditionIcon_) { updateIcon(bestMatch->style.iconCodepoint); currentConditionIcon_ = bestMatch->style.iconCodepoint; } if (bestMatch->style.flags & ConditionStyle::FLAG_USE_TEXT_COLOR) { lv_color_t color = lv_color_make( bestMatch->style.textColor.r, bestMatch->style.textColor.g, bestMatch->style.textColor.b); if (label_) { lv_obj_set_style_text_color(label_, color, 0); } if (iconLabel_) { lv_obj_set_style_text_color(iconLabel_, color, 0); } } if (bestMatch->style.flags & ConditionStyle::FLAG_USE_BG_COLOR) { lv_color_t bgColor = lv_color_make( bestMatch->style.bgColor.r, bestMatch->style.bgColor.g, bestMatch->style.bgColor.b); // Set for default state lv_obj_set_style_bg_color(obj_, bgColor, LV_PART_MAIN | LV_STATE_DEFAULT); // Also set for checked state (toggle buttons) lv_obj_set_style_bg_color(obj_, bgColor, LV_PART_MAIN | LV_STATE_CHECKED); // And for pressed state lv_obj_set_style_bg_color(obj_, bgColor, LV_PART_MAIN | LV_STATE_PRESSED); } if (bestMatch->style.flags & ConditionStyle::FLAG_USE_BG_OPACITY) { lv_obj_set_style_bg_opa(obj_, bestMatch->style.bgOpacity, 0); } if (bestMatch->style.flags & ConditionStyle::FLAG_HIDE) { lv_obj_add_flag(obj_, LV_OBJ_FLAG_HIDDEN); if (shadowObj_ && lv_obj_is_valid(shadowObj_)) { lv_obj_add_flag(shadowObj_, LV_OBJ_FLAG_HIDDEN); } } else { lv_obj_clear_flag(obj_, LV_OBJ_FLAG_HIDDEN); if (shadowObj_ && lv_obj_is_valid(shadowObj_)) { lv_obj_clear_flag(shadowObj_, LV_OBJ_FLAG_HIDDEN); } } return true; } 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_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); } } void ButtonWidget::updateIcon(uint32_t codepoint) { if (codepoint == 0) return; // If no icon label exists yet, we need to create it dynamically if (!iconLabel_ && obj_ && Fonts::hasIconFont()) { // For buttons without a content container, we need to create the icon label if (!contentContainer_) { // Create a simple icon label directly in the button iconLabel_ = lv_label_create(obj_); if (iconLabel_) { lv_obj_clear_flag(iconLabel_, LV_OBJ_FLAG_CLICKABLE); // Position it based on icon position setting if (config_.iconPosition == static_cast(IconPosition::LEFT)) { lv_obj_align(iconLabel_, LV_ALIGN_LEFT_MID, config_.iconPositionX, 0); } else if (config_.iconPosition == static_cast(IconPosition::RIGHT)) { lv_obj_align(iconLabel_, LV_ALIGN_RIGHT_MID, -config_.iconPositionX, 0); } else { lv_obj_align(iconLabel_, LV_ALIGN_CENTER, 0, 0); } } } } if (!iconLabel_) return; // Set the icon text char iconText[5]; encodeUtf8(codepoint, iconText); lv_label_set_text(iconLabel_, iconText); // Apply the correct font size // If iconSize is 0 (no icon was originally configured), use fontSize instead // Default to size 2 (22px) if both are 0 // Icon fonts support sizes 0-13 (14px to 260px) uint8_t sizeIdx = config_.iconSize > 0 ? config_.iconSize : config_.fontSize; if (sizeIdx == 0) sizeIdx = 2; // Default to medium size (22px) if (sizeIdx > 13) sizeIdx = 13; // Cap at max icon font size lv_obj_set_style_text_font(iconLabel_, Fonts::iconFont(sizeIdx), 0); // Apply text color from config lv_obj_set_style_text_color(iconLabel_, lv_color_make( config_.textColor.r, config_.textColor.g, config_.textColor.b), 0); }