diff --git a/.cache/clangd/index/RoomCardWidget.cpp.FA613078500AD19C.idx b/.cache/clangd/index/RoomCardWidget.cpp.FA613078500AD19C.idx index b333027..cb965a0 100644 Binary files a/.cache/clangd/index/RoomCardWidget.cpp.FA613078500AD19C.idx and b/.cache/clangd/index/RoomCardWidget.cpp.FA613078500AD19C.idx differ diff --git a/.cache/clangd/index/SdCard.cpp.485103BC6CBAC699.idx b/.cache/clangd/index/SdCard.cpp.485103BC6CBAC699.idx index 9e38765..8658428 100644 Binary files a/.cache/clangd/index/SdCard.cpp.485103BC6CBAC699.idx and b/.cache/clangd/index/SdCard.cpp.485103BC6CBAC699.idx differ diff --git a/.cache/clangd/index/WidgetConfig.cpp.FD56F9F36C29A5DA.idx b/.cache/clangd/index/WidgetConfig.cpp.FD56F9F36C29A5DA.idx index d42d31f..f2bbd2a 100644 Binary files a/.cache/clangd/index/WidgetConfig.cpp.FD56F9F36C29A5DA.idx and b/.cache/clangd/index/WidgetConfig.cpp.FD56F9F36C29A5DA.idx differ diff --git a/.cache/clangd/index/WidgetConfig.hpp.CAEFE2EEEB2A6996.idx b/.cache/clangd/index/WidgetConfig.hpp.CAEFE2EEEB2A6996.idx index 374d28d..c4b43eb 100644 Binary files a/.cache/clangd/index/WidgetConfig.hpp.CAEFE2EEEB2A6996.idx and b/.cache/clangd/index/WidgetConfig.hpp.CAEFE2EEEB2A6996.idx differ diff --git a/.cache/clangd/index/WidgetFactory.cpp.1026CAEFCA630F22.idx b/.cache/clangd/index/WidgetFactory.cpp.1026CAEFCA630F22.idx index a9fda5e..54dfea7 100644 Binary files a/.cache/clangd/index/WidgetFactory.cpp.1026CAEFCA630F22.idx and b/.cache/clangd/index/WidgetFactory.cpp.1026CAEFCA630F22.idx differ diff --git a/.cache/clangd/index/WidgetManager.cpp.D8CE609DC911F13E.idx b/.cache/clangd/index/WidgetManager.cpp.D8CE609DC911F13E.idx index 6a7ec3f..4d2b1d9 100644 Binary files a/.cache/clangd/index/WidgetManager.cpp.D8CE609DC911F13E.idx and b/.cache/clangd/index/WidgetManager.cpp.D8CE609DC911F13E.idx differ diff --git a/.cache/clangd/index/main.cpp.7C677863E2582AB3.idx b/.cache/clangd/index/main.cpp.7C677863E2582AB3.idx index 4344402..749f9b7 100644 Binary files a/.cache/clangd/index/main.cpp.7C677863E2582AB3.idx and b/.cache/clangd/index/main.cpp.7C677863E2582AB3.idx differ diff --git a/main/WidgetConfig.cpp b/main/WidgetConfig.cpp index 173bc58..b732e00 100644 --- a/main/WidgetConfig.cpp +++ b/main/WidgetConfig.cpp @@ -108,6 +108,8 @@ void WidgetConfig::serialize(uint8_t* buf) const { buf[pos++] = subButtonCount; buf[pos++] = subButtonSize; buf[pos++] = subButtonDistance; + buf[pos++] = subButtonOpacity; + buf[pos++] = cardStyle; for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) { const SubButtonConfig& sb = subButtons[i]; buf[pos++] = sb.iconCodepoint & 0xFF; @@ -253,17 +255,22 @@ void WidgetConfig::deserialize(const uint8_t* buf) { } // RoomCard sub-buttons - if (pos + 3 <= SERIALIZED_SIZE) { + if (pos + 5 <= SERIALIZED_SIZE) { subButtonCount = buf[pos++]; if (subButtonCount > MAX_SUBBUTTONS) subButtonCount = MAX_SUBBUTTONS; subButtonSize = buf[pos++]; if (subButtonSize == 0) subButtonSize = 40; // Default subButtonDistance = buf[pos++]; if (subButtonDistance == 0) subButtonDistance = 80; // Default 80px + subButtonOpacity = buf[pos++]; + if (subButtonOpacity == 0) subButtonOpacity = 255; // Default fully opaque + cardStyle = buf[pos++]; // 0=Bubble, 1=Tile } else { subButtonCount = 0; subButtonSize = 40; subButtonDistance = 80; + subButtonOpacity = 255; + cardStyle = 0; // Default to Bubble style } for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) { diff --git a/main/WidgetConfig.hpp b/main/WidgetConfig.hpp index 0bf58b4..4680094 100644 --- a/main/WidgetConfig.hpp +++ b/main/WidgetConfig.hpp @@ -256,11 +256,13 @@ struct WidgetConfig { uint8_t subButtonCount; uint8_t subButtonSize; // Sub-button size in pixels (default 40) uint8_t subButtonDistance; // Distance from center in pixels (default 80) + uint8_t subButtonOpacity; // Sub-button opacity 0-255 (default 255) + uint8_t cardStyle; // 0=Bubble (round), 1=Tile (rectangular) SubButtonConfig subButtons[MAX_SUBBUTTONS]; // Serialization size (fixed for NVS storage) - // 197 + 1 (subButtonCount) + 1 (subButtonSize) + 1 (subButtonDistance) + 120 (6 subButtons * 20) = 320 - static constexpr size_t SERIALIZED_SIZE = 320; + // 197 + 1 (subButtonCount) + 1 (subButtonSize) + 1 (subButtonDistance) + 1 (subButtonOpacity) + 1 (cardStyle) + 120 (6 subButtons * 20) = 322 + static constexpr size_t SERIALIZED_SIZE = 322; void serialize(uint8_t* buf) const; void deserialize(const uint8_t* buf); diff --git a/main/WidgetManager.cpp b/main/WidgetManager.cpp index c01a23a..696cabf 100644 --- a/main/WidgetManager.cpp +++ b/main/WidgetManager.cpp @@ -1608,6 +1608,8 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const { if (w.type == WidgetType::ROOMCARD) { cJSON_AddNumberToObject(widget, "subButtonSize", w.subButtonSize); cJSON_AddNumberToObject(widget, "subButtonDistance", w.subButtonDistance); + cJSON_AddNumberToObject(widget, "subButtonOpacity", w.subButtonOpacity); + cJSON_AddNumberToObject(widget, "cardStyle", w.cardStyle); } if (w.type == WidgetType::ROOMCARD && w.subButtonCount > 0) { cJSON* subButtons = cJSON_AddArrayToObject(widget, "subButtons"); @@ -1956,6 +1958,18 @@ bool WidgetManager::updateConfigFromJson(const char* json) { } else { w.subButtonDistance = 80; // Default 80px } + cJSON* subButtonOpacity = cJSON_GetObjectItem(widget, "subButtonOpacity"); + if (cJSON_IsNumber(subButtonOpacity)) { + w.subButtonOpacity = subButtonOpacity->valueint; + } else { + w.subButtonOpacity = 255; // Default fully opaque + } + cJSON* cardStyle = cJSON_GetObjectItem(widget, "cardStyle"); + if (cJSON_IsNumber(cardStyle)) { + w.cardStyle = cardStyle->valueint; + } else { + w.cardStyle = 0; // Default to Bubble style + } // RoomCard sub-buttons cJSON* subButtons = cJSON_GetObjectItem(widget, "subButtons"); diff --git a/main/widgets/RoomCardWidget.cpp b/main/widgets/RoomCardWidget.cpp index 72ef22d..b6347cd 100644 --- a/main/widgets/RoomCardWidget.cpp +++ b/main/widgets/RoomCardWidget.cpp @@ -14,11 +14,39 @@ RoomCardWidget::RoomCardWidget(const WidgetConfig& config) { roomName_[0] = '\0'; tempFormat_[0] = '\0'; + subtitle_[0] = '\0'; + humidityFormat_[0] = '\0'; for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) { subButtonStates_[i] = false; } } +RoomCardWidget::~RoomCardWidget() { + // Sub-buttons are on screen parent, not on obj_, so delete them explicitly + for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) { + if (subButtonObjs_[i] && lv_obj_is_valid(subButtonObjs_[i])) { + lv_obj_delete(subButtonObjs_[i]); + subButtonObjs_[i] = nullptr; + } + } +} + +void RoomCardWidget::clearLvglObject() { + // Clear sub-button pointers (they'll be deleted when screen is cleaned) + for (size_t i = 0; i < MAX_SUBBUTTONS; ++i) { + subButtonObjs_[i] = nullptr; + subButtonIcons_[i] = nullptr; + } + bubble_ = nullptr; + roomIcon_ = nullptr; + roomLabel_ = nullptr; + tempLabel_ = nullptr; + subtitleLabel_ = nullptr; + humidityLabel_ = nullptr; + decorIcon_ = nullptr; + obj_ = nullptr; +} + int RoomCardWidget::encodeUtf8(uint32_t codepoint, char* buf) { if (codepoint < 0x80) { buf[0] = static_cast(codepoint); @@ -50,7 +78,11 @@ int RoomCardWidget::encodeUtf8(uint32_t codepoint, char* buf) { void RoomCardWidget::parseText() { roomName_[0] = '\0'; tempFormat_[0] = '\0'; + subtitle_[0] = '\0'; + humidityFormat_[0] = '\0'; + // Format for Bubble: "RoomName\nTempFormat" + // Format for Tile: "RoomName\nSubtitle" (temp/humidity via text2/text3) const char* text = config_.text; if (text && text[0] != '\0') { const char* newline = strchr(text, '\n'); @@ -59,13 +91,33 @@ void RoomCardWidget::parseText() { if (nameLen >= MAX_TEXT_LEN) nameLen = MAX_TEXT_LEN - 1; memcpy(roomName_, text, nameLen); roomName_[nameLen] = '\0'; - strncpy(tempFormat_, newline + 1, MAX_TEXT_LEN - 1); - tempFormat_[MAX_TEXT_LEN - 1] = '\0'; + + if (config_.cardStyle == 0) { + // Bubble: second line is temp format + strncpy(tempFormat_, newline + 1, MAX_TEXT_LEN - 1); + tempFormat_[MAX_TEXT_LEN - 1] = '\0'; + } else { + // Tile: second line is subtitle + strncpy(subtitle_, newline + 1, MAX_TEXT_LEN - 1); + subtitle_[MAX_TEXT_LEN - 1] = '\0'; + } } else { strncpy(roomName_, text, MAX_TEXT_LEN - 1); roomName_[MAX_TEXT_LEN - 1] = '\0'; } } + + // For Tile style, use text2 for temp format and text3 for humidity + if (config_.cardStyle == 1) { + if (config_.text2[0] != '\0') { + strncpy(tempFormat_, config_.text2, MAX_FORMAT_LEN - 1); + tempFormat_[MAX_FORMAT_LEN - 1] = '\0'; + } + if (config_.text3[0] != '\0') { + strncpy(humidityFormat_, config_.text3, MAX_FORMAT_LEN - 1); + humidityFormat_[MAX_FORMAT_LEN - 1] = '\0'; + } + } } void RoomCardWidget::calculateSubButtonPosition(SubButtonPosition pos, int16_t& x, int16_t& y) { @@ -86,26 +138,46 @@ void RoomCardWidget::calculateSubButtonPosition(SubButtonPosition pos, int16_t& y = centerY + static_cast(orbitRadius * sinf(angle)) - subBtnSize / 2; } +void RoomCardWidget::calculateSubButtonPositionTile(uint8_t index, int16_t& x, int16_t& y) { + // For Tile layout: buttons are stacked vertically on the right side + int16_t subBtnSize = config_.subButtonSize > 0 ? config_.subButtonSize : 40; + int16_t gap = 10; // Gap between buttons + int16_t padding = 12; // Padding from edge + + x = config_.width - subBtnSize - padding; + y = padding + index * (subBtnSize + gap); +} + lv_obj_t* RoomCardWidget::create(lv_obj_t* parent) { parseText(); + // Store parent for sub-buttons (they're created on screen level to avoid clipping) + screenParent_ = parent; + // Create main container obj_ = lv_obj_create(parent); lv_obj_remove_style_all(obj_); lv_obj_set_pos(obj_, config_.x, config_.y); lv_obj_set_size(obj_, config_.width, config_.height); lv_obj_clear_flag(obj_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(obj_, LV_OBJ_FLAG_CLICKABLE); - // Create central bubble first - createCentralBubble(); + // Add click handler for navigation + lv_obj_add_event_cb(obj_, bubbleClickCallback, LV_EVENT_CLICKED, this); - // Create sub-buttons - createSubButtons(); + // Create layout based on card style + if (config_.cardStyle == 1) { + createTileLayout(); + createSubButtonsTile(); + } else { + createBubbleLayout(); + createSubButtons(); + } return obj_; } -void RoomCardWidget::createCentralBubble() { +void RoomCardWidget::createBubbleLayout() { // Calculate bubble size (80% of widget size, circular) int16_t minSide = config_.width < config_.height ? config_.width : config_.height; int16_t bubbleSize = (minSide * 80) / 100; @@ -154,6 +226,94 @@ void RoomCardWidget::createCentralBubble() { lv_obj_add_event_cb(bubble_, bubbleClickCallback, LV_EVENT_CLICKED, this); } +void RoomCardWidget::createTileLayout() { + // Tile style: rectangular card with rounded corners + lv_obj_set_style_radius(obj_, config_.borderRadius > 0 ? config_.borderRadius : 16, 0); + lv_obj_set_style_bg_opa(obj_, config_.bgOpacity, 0); + lv_obj_set_style_bg_color(obj_, lv_color_hex(config_.bgColor.toLvColor()), 0); + lv_obj_set_style_clip_corner(obj_, true, 0); + + int16_t padding = 16; + + // Room name (top-left, large) + roomLabel_ = lv_label_create(obj_); + lv_obj_clear_flag(roomLabel_, LV_OBJ_FLAG_CLICKABLE); + lv_label_set_text(roomLabel_, roomName_); + lv_obj_set_pos(roomLabel_, padding, padding); + + // Subtitle (below room name, smaller) + if (subtitle_[0] != '\0') { + subtitleLabel_ = lv_label_create(obj_); + lv_obj_clear_flag(subtitleLabel_, LV_OBJ_FLAG_CLICKABLE); + lv_label_set_text(subtitleLabel_, subtitle_); + lv_obj_set_pos(subtitleLabel_, padding, padding + 28); + } + + // Temperature row (with thermometer icon) + if (tempFormat_[0] != '\0') { + // Container for temp icon + value + lv_obj_t* tempRow = lv_obj_create(obj_); + lv_obj_remove_style_all(tempRow); + lv_obj_set_size(tempRow, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_pos(tempRow, padding, padding + 56); + lv_obj_set_flex_flow(tempRow, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(tempRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_gap(tempRow, 4, 0); + lv_obj_clear_flag(tempRow, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_clear_flag(tempRow, LV_OBJ_FLAG_CLICKABLE); + + // Thermometer icon (U+E1FF or similar) + if (Fonts::hasIconFont()) { + lv_obj_t* tempIcon = lv_label_create(tempRow); + lv_obj_clear_flag(tempIcon, LV_OBJ_FLAG_CLICKABLE); + char iconText[5]; + encodeUtf8(0xf076, iconText); // thermometer icon + lv_label_set_text(tempIcon, iconText); + } + + tempLabel_ = lv_label_create(tempRow); + lv_obj_clear_flag(tempLabel_, LV_OBJ_FLAG_CLICKABLE); + lv_label_set_text(tempLabel_, "--"); + } + + // Humidity row (with water drop icon) + if (humidityFormat_[0] != '\0') { + lv_obj_t* humRow = lv_obj_create(obj_); + lv_obj_remove_style_all(humRow); + lv_obj_set_size(humRow, LV_SIZE_CONTENT, LV_SIZE_CONTENT); + lv_obj_set_pos(humRow, padding + 80, padding + 56); // Next to temp + lv_obj_set_flex_flow(humRow, LV_FLEX_FLOW_ROW); + lv_obj_set_flex_align(humRow, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); + lv_obj_set_style_pad_gap(humRow, 4, 0); + lv_obj_clear_flag(humRow, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_clear_flag(humRow, LV_OBJ_FLAG_CLICKABLE); + + // Water drop icon + if (Fonts::hasIconFont()) { + lv_obj_t* humIcon = lv_label_create(humRow); + lv_obj_clear_flag(humIcon, LV_OBJ_FLAG_CLICKABLE); + char iconText[5]; + encodeUtf8(0xe798, iconText); // humidity/water icon + lv_label_set_text(humIcon, iconText); + } + + humidityLabel_ = lv_label_create(humRow); + lv_obj_clear_flag(humidityLabel_, LV_OBJ_FLAG_CLICKABLE); + lv_label_set_text(humidityLabel_, "--"); + } + + // Large decorative icon (bottom-left, partially visible) + if (config_.iconCodepoint > 0 && Fonts::hasIconFont()) { + decorIcon_ = lv_label_create(obj_); + lv_obj_clear_flag(decorIcon_, LV_OBJ_FLAG_CLICKABLE); + char iconText[5]; + encodeUtf8(config_.iconCodepoint, iconText); + lv_label_set_text(decorIcon_, iconText); + // Position at bottom-left, slightly outside + lv_obj_set_pos(decorIcon_, -20, config_.height - 100); + } +} + void RoomCardWidget::createSubButtons() { // Sub-button size (configurable, default 40) int16_t subBtnSize = config_.subButtonSize > 0 ? config_.subButtonSize : 40; @@ -162,8 +322,8 @@ void RoomCardWidget::createSubButtons() { const SubButtonConfig& cfg = config_.subButtons[i]; if (!cfg.enabled) continue; - // Create sub-button object - lv_obj_t* btn = lv_obj_create(obj_); + // Create sub-button on screen parent (not on widget container) to avoid clipping + lv_obj_t* btn = lv_obj_create(screenParent_); lv_obj_remove_style_all(btn); lv_obj_set_size(btn, subBtnSize, subBtnSize); lv_obj_clear_flag(btn, LV_OBJ_FLAG_SCROLLABLE); @@ -171,12 +331,64 @@ void RoomCardWidget::createSubButtons() { // Circular shape lv_obj_set_style_radius(btn, LV_RADIUS_CIRCLE, 0); - lv_obj_set_style_bg_opa(btn, 255, 0); + lv_obj_set_style_bg_opa(btn, config_.subButtonOpacity, 0); - // Position using circle geometry - int16_t x, y; - calculateSubButtonPosition(cfg.position, x, y); - lv_obj_set_pos(btn, x, y); + // Position using circle geometry (relative to widget center) + int16_t relX, relY; + calculateSubButtonPosition(cfg.position, relX, relY); + // Convert to absolute screen position + int16_t absX = config_.x + relX; + int16_t absY = config_.y + relY; + lv_obj_set_pos(btn, absX, absY); + + // Create icon + if (cfg.iconCodepoint > 0 && Fonts::hasIconFont()) { + lv_obj_t* icon = lv_label_create(btn); + lv_obj_clear_flag(icon, LV_OBJ_FLAG_CLICKABLE); + char iconText[5]; + encodeUtf8(cfg.iconCodepoint, iconText); + lv_label_set_text(icon, iconText); + lv_obj_center(icon); + subButtonIcons_[i] = icon; + } + + // Store index in user_data for click handler + lv_obj_set_user_data(btn, reinterpret_cast(static_cast(i))); + lv_obj_add_event_cb(btn, subButtonClickCallback, LV_EVENT_CLICKED, this); + + subButtonObjs_[i] = btn; + subButtonStates_[i] = false; + + // Apply initial color (OFF state) + updateSubButtonColor(i); + } +} + +void RoomCardWidget::createSubButtonsTile() { + // For Tile layout: buttons are positioned on the right side, vertically stacked + int16_t subBtnSize = config_.subButtonSize > 0 ? config_.subButtonSize : 40; + + for (uint8_t i = 0; i < config_.subButtonCount && i < MAX_SUBBUTTONS; ++i) { + const SubButtonConfig& cfg = config_.subButtons[i]; + if (!cfg.enabled) continue; + + // Create sub-button on screen parent + lv_obj_t* btn = lv_obj_create(screenParent_); + lv_obj_remove_style_all(btn); + lv_obj_set_size(btn, subBtnSize, subBtnSize); + lv_obj_clear_flag(btn, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_add_flag(btn, LV_OBJ_FLAG_CLICKABLE); + + // Circular shape + lv_obj_set_style_radius(btn, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_opa(btn, config_.subButtonOpacity, 0); + + // Position on right side + int16_t relX, relY; + calculateSubButtonPositionTile(i, relX, relY); + int16_t absX = config_.x + relX; + int16_t absY = config_.y + relY; + lv_obj_set_pos(btn, absX, absY); // Create icon if (cfg.iconCodepoint > 0 && Fonts::hasIconFont()) { @@ -204,42 +416,109 @@ void RoomCardWidget::createSubButtons() { void RoomCardWidget::applyStyle() { if (!obj_) return; - // Apply shadow to bubble if enabled - if (bubble_ && config_.shadow.enabled) { - lv_obj_set_style_shadow_width(bubble_, config_.shadow.blur, 0); - lv_obj_set_style_shadow_ofs_x(bubble_, config_.shadow.offsetX, 0); - lv_obj_set_style_shadow_ofs_y(bubble_, config_.shadow.offsetY, 0); - lv_obj_set_style_shadow_color(bubble_, lv_color_hex(config_.shadow.color.toLvColor()), 0); - lv_obj_set_style_shadow_opa(bubble_, 255, 0); - } - - // Font sizes - const lv_font_t* iconFont = Fonts::iconFont(config_.iconSize); - const lv_font_t* textFont = Fonts::bySizeIndex(config_.fontSize); - const lv_font_t* labelFont = Fonts::bySizeIndex(config_.fontSize > 0 ? config_.fontSize - 1 : 0); lv_color_t textColor = lv_color_hex(config_.textColor.toLvColor()); - // Style room icon - if (roomIcon_ && iconFont) { - lv_obj_set_style_text_font(roomIcon_, iconFont, 0); - lv_obj_set_style_text_color(roomIcon_, textColor, 0); - } + if (config_.cardStyle == 1) { + // Tile style + // Apply border if shadow is enabled (used as accent border) + if (config_.shadow.enabled) { + lv_obj_set_style_border_width(obj_, 3, 0); + lv_obj_set_style_border_color(obj_, lv_color_hex(config_.shadow.color.toLvColor()), 0); + lv_obj_set_style_border_opa(obj_, 255, 0); + } - // Style temperature - if (tempLabel_ && textFont) { - lv_obj_set_style_text_font(tempLabel_, textFont, 0); - lv_obj_set_style_text_color(tempLabel_, textColor, 0); - } + // Room name - large font + if (roomLabel_) { + const lv_font_t* nameFont = Fonts::bySizeIndex(config_.fontSize); + lv_obj_set_style_text_font(roomLabel_, nameFont, 0); + lv_obj_set_style_text_color(roomLabel_, textColor, 0); + } - // Style room label - if (roomLabel_ && labelFont) { - lv_obj_set_style_text_font(roomLabel_, labelFont, 0); - lv_obj_set_style_text_color(roomLabel_, lv_color_hex(config_.textColor.toLvColor()), 0); - lv_obj_set_style_text_opa(roomLabel_, 180, 0); // Slightly dimmed + // Subtitle - smaller, dimmed + if (subtitleLabel_) { + const lv_font_t* subFont = Fonts::bySizeIndex(config_.fontSize > 1 ? config_.fontSize - 2 : 0); + lv_obj_set_style_text_font(subtitleLabel_, subFont, 0); + lv_obj_set_style_text_color(subtitleLabel_, textColor, 0); + lv_obj_set_style_text_opa(subtitleLabel_, 150, 0); + } + + // Temp and humidity - medium font + const lv_font_t* valueFont = Fonts::bySizeIndex(config_.fontSize > 0 ? config_.fontSize - 1 : 0); + const lv_font_t* iconFont = Fonts::iconFont(1); // Small icons + + if (tempLabel_) { + lv_obj_set_style_text_font(tempLabel_, valueFont, 0); + lv_obj_set_style_text_color(tempLabel_, textColor, 0); + // Style the icon in the same row + lv_obj_t* parent = lv_obj_get_parent(tempLabel_); + if (parent && lv_obj_get_child_count(parent) > 0) { + lv_obj_t* iconLabel = lv_obj_get_child(parent, 0); + if (iconLabel && iconFont) { + lv_obj_set_style_text_font(iconLabel, iconFont, 0); + lv_obj_set_style_text_color(iconLabel, textColor, 0); + } + } + } + + if (humidityLabel_) { + lv_obj_set_style_text_font(humidityLabel_, valueFont, 0); + lv_obj_set_style_text_color(humidityLabel_, textColor, 0); + lv_obj_t* parent = lv_obj_get_parent(humidityLabel_); + if (parent && lv_obj_get_child_count(parent) > 0) { + lv_obj_t* iconLabel = lv_obj_get_child(parent, 0); + if (iconLabel && iconFont) { + lv_obj_set_style_text_font(iconLabel, iconFont, 0); + lv_obj_set_style_text_color(iconLabel, textColor, 0); + } + } + } + + // Large decorative icon + if (decorIcon_) { + const lv_font_t* bigIconFont = Fonts::iconFont(5); // Largest icon + if (bigIconFont) { + lv_obj_set_style_text_font(decorIcon_, bigIconFont, 0); + lv_obj_set_style_text_color(decorIcon_, textColor, 0); + lv_obj_set_style_text_opa(decorIcon_, 60, 0); // Very transparent + } + } + } else { + // Bubble style + // Apply shadow to bubble if enabled + if (bubble_ && config_.shadow.enabled) { + lv_obj_set_style_shadow_width(bubble_, config_.shadow.blur, 0); + lv_obj_set_style_shadow_ofs_x(bubble_, config_.shadow.offsetX, 0); + lv_obj_set_style_shadow_ofs_y(bubble_, config_.shadow.offsetY, 0); + lv_obj_set_style_shadow_color(bubble_, lv_color_hex(config_.shadow.color.toLvColor()), 0); + lv_obj_set_style_shadow_opa(bubble_, 255, 0); + } + + // Font sizes + const lv_font_t* iconFont = Fonts::iconFont(config_.iconSize); + const lv_font_t* textFont = Fonts::bySizeIndex(config_.fontSize); + const lv_font_t* labelFont = Fonts::bySizeIndex(config_.fontSize > 0 ? config_.fontSize - 1 : 0); + + // Style room icon + if (roomIcon_ && iconFont) { + lv_obj_set_style_text_font(roomIcon_, iconFont, 0); + lv_obj_set_style_text_color(roomIcon_, textColor, 0); + } + + // Style temperature + if (tempLabel_ && textFont) { + lv_obj_set_style_text_font(tempLabel_, textFont, 0); + lv_obj_set_style_text_color(tempLabel_, textColor, 0); + } + + // Style room label + if (roomLabel_ && labelFont) { + lv_obj_set_style_text_font(roomLabel_, labelFont, 0); + lv_obj_set_style_text_color(roomLabel_, textColor, 0); + lv_obj_set_style_text_opa(roomLabel_, 180, 0); // Slightly dimmed + } } // Style sub-buttons - adjust icon size based on button size - // subButtonSize: 30-40 -> font 1, 41-55 -> font 2, 56+ -> font 3 uint8_t subBtnFontIdx = 1; // Default small if (config_.subButtonSize > 55) { subBtnFontIdx = 3; @@ -272,6 +551,14 @@ void RoomCardWidget::updateTemperature(float value) { lv_label_set_text(tempLabel_, buf); } +void RoomCardWidget::updateHumidity(float value) { + if (!humidityLabel_ || humidityFormat_[0] == '\0') return; + + char buf[32]; + snprintf(buf, sizeof(buf), humidityFormat_, value); + lv_label_set_text(humidityLabel_, buf); +} + void RoomCardWidget::onKnxValue(float value) { cachedPrimaryValue_ = value; hasCachedPrimary_ = true; diff --git a/main/widgets/RoomCardWidget.hpp b/main/widgets/RoomCardWidget.hpp index cb737fd..8d0daac 100644 --- a/main/widgets/RoomCardWidget.hpp +++ b/main/widgets/RoomCardWidget.hpp @@ -5,6 +5,7 @@ class RoomCardWidget : public Widget { public: explicit RoomCardWidget(const WidgetConfig& config); + ~RoomCardWidget() override; lv_obj_t* create(lv_obj_t* parent) override; void applyStyle() override; void onKnxValue(float value) override; // Temperature update @@ -14,13 +15,24 @@ public: void onSubButtonStatus(uint8_t index, bool value); void sendSubButtonToggle(uint8_t index); + // Override to also clear sub-buttons + void clearLvglObject(); + private: - // Central bubble elements + // Parent (screen) for sub-buttons to avoid clipping + lv_obj_t* screenParent_ = nullptr; + + // Bubble style elements (cardStyle=0) lv_obj_t* bubble_ = nullptr; lv_obj_t* roomIcon_ = nullptr; lv_obj_t* roomLabel_ = nullptr; lv_obj_t* tempLabel_ = nullptr; + // Tile style elements (cardStyle=1) + lv_obj_t* subtitleLabel_ = nullptr; + lv_obj_t* humidityLabel_ = nullptr; + lv_obj_t* decorIcon_ = nullptr; // Large decorative icon bottom-left + // Sub-button elements lv_obj_t* subButtonObjs_[MAX_SUBBUTTONS] = {}; lv_obj_t* subButtonIcons_[MAX_SUBBUTTONS] = {}; @@ -29,16 +41,22 @@ private: // Cached config values char roomName_[MAX_TEXT_LEN] = {0}; char tempFormat_[MAX_TEXT_LEN] = {0}; + char subtitle_[MAX_TEXT_LEN] = {0}; + char humidityFormat_[MAX_FORMAT_LEN] = {0}; // Layout helpers void parseText(); - void createCentralBubble(); + void createBubbleLayout(); // Bubble style (round) + void createTileLayout(); // Tile style (rectangular) void createSubButtons(); + void createSubButtonsTile(); // SubButtons for tile (right side) void updateSubButtonColor(uint8_t index); void updateTemperature(float value); + void updateHumidity(float value); // Geometry calculations void calculateSubButtonPosition(SubButtonPosition pos, int16_t& x, int16_t& y); + void calculateSubButtonPositionTile(uint8_t index, int16_t& x, int16_t& y); // Event handlers static void bubbleClickCallback(lv_event_t* e); diff --git a/web-interface/src/components/SidebarRight.vue b/web-interface/src/components/SidebarRight.vue index 8edb559..123a805 100644 --- a/web-interface/src/components/SidebarRight.vue +++ b/web-interface/src/components/SidebarRight.vue @@ -395,6 +395,16 @@ + +

Karten-Stil

+
+ + +
+

Sub-Buttons

@@ -419,6 +429,10 @@ px
+
+ + +
@@ -585,7 +599,7 @@
-