623 lines
23 KiB
C++
623 lines
23 KiB
C++
#include "RoomCardWidget.hpp"
|
|
#include "../Fonts.hpp"
|
|
#include "../WidgetManager.hpp"
|
|
#include <cstring>
|
|
#include <cstdio>
|
|
#include <cmath>
|
|
|
|
#ifndef M_PI
|
|
#define M_PI 3.14159265358979323846
|
|
#endif
|
|
|
|
RoomCardWidget::RoomCardWidget(const WidgetConfig& config)
|
|
: Widget(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<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 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');
|
|
if (newline) {
|
|
size_t nameLen = static_cast<size_t>(newline - text);
|
|
if (nameLen >= MAX_TEXT_LEN) nameLen = MAX_TEXT_LEN - 1;
|
|
memcpy(roomName_, text, nameLen);
|
|
roomName_[nameLen] = '\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) {
|
|
int16_t centerX = config_.width / 2;
|
|
int16_t centerY = config_.height / 2;
|
|
|
|
// Sub-button size (configurable, default 40)
|
|
int16_t subBtnSize = config_.subButtonSize > 0 ? config_.subButtonSize : 40;
|
|
|
|
// Sub-buttons orbit radius in pixels (default 80)
|
|
int16_t orbitRadius = config_.subButtonDistance > 0 ? config_.subButtonDistance : 80;
|
|
|
|
// Calculate angle: 0=TOP, going clockwise, 8 positions = 45 degrees each
|
|
float angle = static_cast<float>(static_cast<uint8_t>(pos)) * (M_PI / 4.0f) - (M_PI / 2.0f);
|
|
|
|
// Calculate position (center of sub-button)
|
|
x = centerX + static_cast<int16_t>(orbitRadius * cosf(angle)) - subBtnSize / 2;
|
|
y = centerY + static_cast<int16_t>(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);
|
|
|
|
// Add click handler for navigation
|
|
lv_obj_add_event_cb(obj_, bubbleClickCallback, LV_EVENT_CLICKED, this);
|
|
|
|
// Create layout based on card style
|
|
if (config_.cardStyle == 1) {
|
|
createTileLayout();
|
|
createSubButtonsTile();
|
|
} else {
|
|
createBubbleLayout();
|
|
createSubButtons();
|
|
}
|
|
|
|
return obj_;
|
|
}
|
|
|
|
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;
|
|
|
|
// Create bubble container (centered)
|
|
bubble_ = lv_obj_create(obj_);
|
|
lv_obj_remove_style_all(bubble_);
|
|
lv_obj_set_size(bubble_, bubbleSize, bubbleSize);
|
|
lv_obj_center(bubble_);
|
|
lv_obj_clear_flag(bubble_, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_add_flag(bubble_, LV_OBJ_FLAG_CLICKABLE);
|
|
|
|
// Circular shape
|
|
lv_obj_set_style_radius(bubble_, LV_RADIUS_CIRCLE, 0);
|
|
lv_obj_set_style_bg_opa(bubble_, config_.bgOpacity, 0);
|
|
lv_obj_set_style_bg_color(bubble_, lv_color_hex(config_.bgColor.toLvColor()), 0);
|
|
|
|
// Set up flex layout for bubble content
|
|
lv_obj_set_flex_flow(bubble_, LV_FLEX_FLOW_COLUMN);
|
|
lv_obj_set_flex_align(bubble_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
|
lv_obj_set_style_pad_all(bubble_, 8, 0);
|
|
lv_obj_set_style_pad_gap(bubble_, 2, 0);
|
|
|
|
// Room icon
|
|
if (config_.iconCodepoint > 0 && Fonts::hasIconFont()) {
|
|
roomIcon_ = lv_label_create(bubble_);
|
|
lv_obj_clear_flag(roomIcon_, LV_OBJ_FLAG_CLICKABLE);
|
|
char iconText[5];
|
|
encodeUtf8(config_.iconCodepoint, iconText);
|
|
lv_label_set_text(roomIcon_, iconText);
|
|
}
|
|
|
|
// Temperature label
|
|
tempLabel_ = lv_label_create(bubble_);
|
|
lv_obj_clear_flag(tempLabel_, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_label_set_text(tempLabel_, tempFormat_[0] != '\0' ? "--" : "");
|
|
|
|
// Room name label
|
|
if (roomName_[0] != '\0') {
|
|
roomLabel_ = lv_label_create(bubble_);
|
|
lv_obj_clear_flag(roomLabel_, LV_OBJ_FLAG_CLICKABLE);
|
|
lv_label_set_text(roomLabel_, roomName_);
|
|
}
|
|
|
|
// Click handler for navigation
|
|
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;
|
|
|
|
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 (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);
|
|
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 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<void*>(static_cast<uintptr_t>(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()) {
|
|
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<void*>(static_cast<uintptr_t>(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::applyStyle() {
|
|
if (!obj_) return;
|
|
|
|
lv_color_t textColor = lv_color_hex(config_.textColor.toLvColor());
|
|
|
|
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);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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
|
|
uint8_t subBtnFontIdx = 1; // Default small
|
|
if (config_.subButtonSize > 55) {
|
|
subBtnFontIdx = 3;
|
|
} else if (config_.subButtonSize > 40) {
|
|
subBtnFontIdx = 2;
|
|
}
|
|
const lv_font_t* subBtnIconFont = Fonts::iconFont(subBtnFontIdx);
|
|
for (uint8_t i = 0; i < config_.subButtonCount && i < MAX_SUBBUTTONS; ++i) {
|
|
if (subButtonIcons_[i] && subBtnIconFont) {
|
|
lv_obj_set_style_text_font(subButtonIcons_[i], subBtnIconFont, 0);
|
|
lv_obj_set_style_text_color(subButtonIcons_[i], lv_color_white(), 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void RoomCardWidget::updateSubButtonColor(uint8_t index) {
|
|
if (index >= MAX_SUBBUTTONS || !subButtonObjs_[index]) return;
|
|
|
|
const SubButtonConfig& cfg = config_.subButtons[index];
|
|
const Color& color = subButtonStates_[index] ? cfg.colorOn : cfg.colorOff;
|
|
|
|
lv_obj_set_style_bg_color(subButtonObjs_[index], lv_color_hex(color.toLvColor()), 0);
|
|
}
|
|
|
|
void RoomCardWidget::updateTemperature(float value) {
|
|
if (!tempLabel_ || tempFormat_[0] == '\0') return;
|
|
|
|
char buf[32];
|
|
snprintf(buf, sizeof(buf), tempFormat_, 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;
|
|
updateTemperature(value);
|
|
}
|
|
|
|
void RoomCardWidget::onKnxSwitch(bool value) {
|
|
// Not used directly - sub-button status is handled via onSubButtonStatus
|
|
(void)value;
|
|
}
|
|
|
|
void RoomCardWidget::onSubButtonStatus(uint8_t index, bool value) {
|
|
if (index >= MAX_SUBBUTTONS) return;
|
|
|
|
subButtonStates_[index] = value;
|
|
updateSubButtonColor(index);
|
|
}
|
|
|
|
void RoomCardWidget::sendSubButtonToggle(uint8_t index) {
|
|
if (index >= config_.subButtonCount || index >= MAX_SUBBUTTONS) return;
|
|
|
|
const SubButtonConfig& cfg = config_.subButtons[index];
|
|
if (cfg.action == SubButtonAction::TOGGLE_KNX && cfg.knxAddrWrite > 0) {
|
|
bool newState = !subButtonStates_[index];
|
|
// Send KNX toggle via WidgetManager
|
|
WidgetManager::instance().sendKnxSwitch(cfg.knxAddrWrite, newState);
|
|
// Optimistically update local state
|
|
subButtonStates_[index] = newState;
|
|
updateSubButtonColor(index);
|
|
}
|
|
}
|
|
|
|
void RoomCardWidget::bubbleClickCallback(lv_event_t* e) {
|
|
RoomCardWidget* widget = static_cast<RoomCardWidget*>(lv_event_get_user_data(e));
|
|
if (!widget) return;
|
|
|
|
// Handle navigation based on action
|
|
if (widget->config_.action == ButtonAction::JUMP) {
|
|
WidgetManager::instance().navigateToScreen(widget->config_.targetScreen);
|
|
} else if (widget->config_.action == ButtonAction::BACK) {
|
|
WidgetManager::instance().navigateBack();
|
|
}
|
|
}
|
|
|
|
void RoomCardWidget::subButtonClickCallback(lv_event_t* e) {
|
|
RoomCardWidget* widget = static_cast<RoomCardWidget*>(lv_event_get_user_data(e));
|
|
lv_obj_t* target = static_cast<lv_obj_t*>(lv_event_get_target(e));
|
|
if (!widget || !target) return;
|
|
|
|
uint8_t index = static_cast<uint8_t>(reinterpret_cast<uintptr_t>(lv_obj_get_user_data(target)));
|
|
|
|
if (index < widget->config_.subButtonCount && index < MAX_SUBBUTTONS) {
|
|
const SubButtonConfig& cfg = widget->config_.subButtons[index];
|
|
|
|
if (cfg.action == SubButtonAction::TOGGLE_KNX) {
|
|
widget->sendSubButtonToggle(index);
|
|
} else if (cfg.action == SubButtonAction::NAVIGATE) {
|
|
WidgetManager::instance().navigateToScreen(cfg.targetScreen);
|
|
}
|
|
}
|
|
}
|