knxdisplay/main/widgets/ArcWidget.cpp
2026-02-10 21:40:55 +01:00

238 lines
9.0 KiB
C++

#include "ArcWidget.hpp"
#include <algorithm>
#include <cmath>
#include <cstdio>
ArcWidget::ArcWidget(const WidgetConfig& config)
: Widget(config)
{
}
static int32_t clampToRange(float value, int32_t minValue, int32_t maxValue) {
if (maxValue < minValue) std::swap(maxValue, minValue);
if (value < minValue) value = static_cast<float>(minValue);
if (value > maxValue) value = static_cast<float>(maxValue);
return static_cast<int32_t>(std::lround(value));
}
lv_obj_t* ArcWidget::create(lv_obj_t* parent) {
obj_ = lv_arc_create(parent);
if (!obj_) return nullptr;
lv_obj_set_pos(obj_, config_.x, config_.y);
lv_obj_set_size(obj_,
config_.width > 0 ? config_.width : 180,
config_.height > 0 ? config_.height : 180);
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_remove_flag(obj_, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_flag(obj_, LV_OBJ_FLAG_OVERFLOW_VISIBLE);
lv_obj_set_style_pad_all(obj_, 0, 0);
lv_arc_set_rotation(obj_, 0);
lv_arc_set_mode(obj_, LV_ARC_MODE_NORMAL);
lv_arc_set_bg_angles(obj_, 135, 45);
int32_t minValue = config_.arcMin;
int32_t maxValue = config_.arcMax;
if (maxValue <= minValue) {
minValue = 0;
maxValue = 100;
}
lv_arc_set_range(obj_, minValue, maxValue);
lastValue_ = static_cast<float>(minValue);
lv_arc_set_value(obj_, minValue);
// Create scale as child of arc
scale_ = lv_scale_create(obj_);
if (scale_) {
lv_obj_clear_flag(scale_, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_remove_flag(scale_, LV_OBJ_FLAG_CLICKABLE);
lv_obj_add_flag(scale_, LV_OBJ_FLAG_OVERFLOW_VISIBLE);
lv_obj_set_style_bg_opa(scale_, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(scale_, 0, 0);
lv_obj_set_style_pad_all(scale_, 0, 0);
}
valueLabel_ = lv_label_create(obj_);
if (valueLabel_) {
lv_obj_center(valueLabel_);
updateValueLabel(lastValue_);
}
return obj_;
}
void ArcWidget::applyStyle() {
if (!obj_) return;
applyCommonStyle();
lv_obj_set_style_bg_opa(obj_, LV_OPA_TRANSP, 0);
int32_t minValue = config_.arcMin;
int32_t maxValue = config_.arcMax;
if (maxValue <= minValue) {
minValue = 0;
maxValue = 100;
}
lv_arc_set_range(obj_, minValue, maxValue);
int16_t arcWidth = config_.borderRadius > 0 ? static_cast<int16_t>(config_.borderRadius) : 12;
if (arcWidth < 2) arcWidth = 2;
if (arcWidth > 48) arcWidth = 48;
Color trackCol = themeColor(config_.bgColor);
lv_color_t trackColor = lv_color_make(trackCol.r, trackCol.g, trackCol.b);
uint8_t trackOpa = config_.bgOpacity > 0 ? config_.bgOpacity : static_cast<uint8_t>(LV_OPA_40);
Color indicatorCol = themeColor(config_.textColor);
lv_color_t indicatorColor = lv_color_make(indicatorCol.r, indicatorCol.g, indicatorCol.b);
lv_obj_set_style_arc_width(obj_, arcWidth, LV_PART_MAIN);
lv_obj_set_style_arc_color(obj_, trackColor, LV_PART_MAIN);
lv_obj_set_style_arc_opa(obj_, trackOpa, LV_PART_MAIN);
lv_obj_set_style_arc_rounded(obj_, true, LV_PART_MAIN);
lv_obj_set_style_arc_width(obj_, arcWidth, LV_PART_INDICATOR);
lv_obj_set_style_arc_color(obj_, indicatorColor, LV_PART_INDICATOR);
lv_obj_set_style_arc_opa(obj_, LV_OPA_COVER, LV_PART_INDICATOR);
lv_obj_set_style_arc_rounded(obj_, true, LV_PART_INDICATOR);
int16_t knobSize = std::max<int16_t>(8, static_cast<int16_t>(arcWidth + 6));
lv_obj_set_style_arc_width(obj_, knobSize, LV_PART_KNOB);
lv_obj_set_style_bg_color(obj_, indicatorColor, LV_PART_KNOB);
lv_obj_set_style_bg_opa(obj_, LV_OPA_COVER, LV_PART_KNOB);
if (scale_) {
// Use config dimensions directly, not from arc object (which may have content area limitations)
int16_t scaleWidth = config_.width > 0 ? static_cast<int16_t>(config_.width) : 180;
int16_t scaleHeight = config_.height > 0 ? static_cast<int16_t>(config_.height) : 180;
lv_obj_set_size(scale_, scaleWidth, scaleHeight);
lv_obj_set_pos(scale_, 0, 0);
lv_obj_set_align(scale_, LV_ALIGN_CENTER);
lv_scale_set_mode(scale_, LV_SCALE_MODE_ROUND_INNER);
lv_scale_set_angle_range(scale_, 270);
lv_scale_set_rotation(scale_, 135);
lv_scale_set_range(scale_, minValue, maxValue);
lv_scale_set_total_tick_count(scale_, 21);
lv_scale_set_major_tick_every(scale_, 5);
lv_scale_set_label_show(scale_, true);
int16_t majorLen = std::max<int16_t>(3, static_cast<int16_t>(arcWidth / 3 + 1));
int16_t minorLen = std::max<int16_t>(2, static_cast<int16_t>(arcWidth / 4 + 1));
lv_obj_set_style_length(scale_, majorLen, LV_PART_INDICATOR);
lv_obj_set_style_length(scale_, minorLen, LV_PART_ITEMS);
// Use radial_offset to position ticks at inner edge of arc stroke.
// Positive arcScaleOffset should move the scale outward.
int16_t scaleOffset = static_cast<int16_t>(-config_.arcScaleOffset);
int16_t tickOffset = static_cast<int16_t>(-arcWidth + scaleOffset);
lv_obj_set_style_radial_offset(scale_, tickOffset, LV_PART_INDICATOR);
lv_obj_set_style_radial_offset(scale_, tickOffset, LV_PART_ITEMS);
lv_obj_set_style_arc_width(scale_, 1, LV_PART_MAIN);
lv_obj_set_style_arc_opa(scale_, LV_OPA_TRANSP, LV_PART_MAIN);
Color scaleCol = themeColor(config_.arcScaleColor);
lv_color_t scaleColor = lv_color_make(scaleCol.r, scaleCol.g, scaleCol.b);
lv_obj_set_style_line_color(scale_, scaleColor, LV_PART_INDICATOR);
lv_obj_set_style_line_color(scale_, scaleColor, LV_PART_ITEMS);
lv_obj_set_style_line_width(scale_, 2, LV_PART_INDICATOR);
lv_obj_set_style_line_width(scale_, 1, LV_PART_ITEMS);
lv_obj_set_style_line_opa(scale_, LV_OPA_80, LV_PART_INDICATOR);
lv_obj_set_style_line_opa(scale_, LV_OPA_40, LV_PART_ITEMS);
uint8_t scaleFont = config_.fontSize > 0 ? static_cast<uint8_t>(config_.fontSize - 1) : config_.fontSize;
lv_obj_set_style_text_color(scale_, scaleColor, LV_PART_INDICATOR);
lv_obj_set_style_text_opa(scale_, LV_OPA_80, LV_PART_INDICATOR);
lv_obj_set_style_text_font(scale_, getFontBySize(scaleFont), LV_PART_INDICATOR);
lv_obj_set_style_transform_rotation(scale_, LV_SCALE_LABEL_ROTATE_MATCH_TICKS | LV_SCALE_LABEL_ROTATE_KEEP_UPRIGHT,
LV_PART_INDICATOR);
int16_t labelPad = static_cast<int16_t>(-(arcWidth / 2 + 4) + scaleOffset);
lv_obj_set_style_pad_radial(scale_, labelPad, LV_PART_INDICATOR);
}
if (valueLabel_) {
Color valueCol = themeColor(config_.arcValueColor);
lv_color_t valueColor = lv_color_make(valueCol.r, valueCol.g, valueCol.b);
lv_obj_set_style_text_color(valueLabel_, valueColor, 0);
lv_obj_set_style_text_font(valueLabel_, getFontBySize(config_.arcValueFontSize), 0);
lv_obj_center(valueLabel_);
updateValueLabel(lastValue_);
}
}
void ArcWidget::onKnxValue(float value) {
if (!obj_) return;
switch (config_.textSource) {
case TextSource::KNX_DPT_TEMP:
case TextSource::KNX_DPT_PERCENT:
case TextSource::KNX_DPT_POWER:
case TextSource::KNX_DPT_ENERGY:
case TextSource::KNX_DPT_DECIMALFACTOR:
lastValue_ = value;
lv_arc_set_value(obj_, clampToRange(value, config_.arcMin, config_.arcMax));
updateValueLabel(value);
break;
default:
break;
}
}
void ArcWidget::onKnxSwitch(bool value) {
if (!obj_) return;
if (config_.textSource != TextSource::KNX_DPT_SWITCH) return;
int32_t minValue = config_.arcMin;
int32_t maxValue = config_.arcMax;
if (maxValue <= minValue) {
minValue = 0;
maxValue = 100;
}
lastValue_ = value ? static_cast<float>(maxValue) : static_cast<float>(minValue);
lv_arc_set_value(obj_, value ? maxValue : minValue);
updateValueLabel(lastValue_);
}
const char* ArcWidget::unitSuffix(uint8_t unit) {
switch (unit) {
case 1: return "%";
case 2: return "C";
default: return "";
}
}
void ArcWidget::updateValueLabel(float value) {
if (!valueLabel_) return;
if (!config_.arcShowValue) {
lv_obj_add_flag(valueLabel_, LV_OBJ_FLAG_HIDDEN);
return;
}
lv_obj_clear_flag(valueLabel_, LV_OBJ_FLAG_HIDDEN);
int32_t minValue = config_.arcMin;
int32_t maxValue = config_.arcMax;
if (maxValue <= minValue) {
minValue = 0;
maxValue = 100;
}
float clamped = value;
if (clamped < minValue) clamped = static_cast<float>(minValue);
if (clamped > maxValue) clamped = static_cast<float>(maxValue);
char buf[24];
const char* suffix = unitSuffix(config_.arcUnit);
bool useDecimal = (config_.textSource == TextSource::KNX_DPT_TEMP);
if (useDecimal) {
std::snprintf(buf, sizeof(buf), "%.1f%s", clamped, suffix);
} else {
std::snprintf(buf, sizeof(buf), "%.0f%s", clamped, suffix);
}
lv_label_set_text(valueLabel_, buf);
}