From ef66dfaa246da70e747a3267b9b340ed398dc541 Mon Sep 17 00:00:00 2001 From: Thomas Peterson Date: Mon, 26 Jan 2026 09:26:11 +0100 Subject: [PATCH] Fixes --- main/CMakeLists.txt | 2 + main/WidgetConfig.hpp | 2 + main/widgets/TabPageWidget.cpp | 43 +++++ main/widgets/TabPageWidget.hpp | 12 ++ main/widgets/TabViewWidget.cpp | 53 ++++++ main/widgets/TabViewWidget.hpp | 12 ++ main/widgets/WidgetFactory.cpp | 6 + web-interface/src/components/CanvasArea.vue | 24 ++- web-interface/src/components/SidebarLeft.vue | 4 + web-interface/src/components/SidebarRight.vue | 21 +++ web-interface/src/components/TreeItem.vue | 177 +++++++++++++---- .../src/components/WidgetElement.vue | 178 ++++++++++++++++-- web-interface/src/constants.js | 55 +++++- web-interface/src/stores/editor.js | 85 ++++++++- web-interface/src/style.css | 1 - web-interface/src/utils.js | 1 + 16 files changed, 606 insertions(+), 70 deletions(-) create mode 100644 main/widgets/TabPageWidget.cpp create mode 100644 main/widgets/TabPageWidget.hpp create mode 100644 main/widgets/TabViewWidget.cpp create mode 100644 main/widgets/TabViewWidget.hpp diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index feb416c..947d15b 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -5,6 +5,8 @@ idf_component_register(SRCS "KnxWorker.cpp" "Nvs.cpp" "main.cpp" "Display.cpp" " "widgets/LedWidget.cpp" "widgets/WidgetFactory.cpp" "widgets/IconWidget.cpp" + "widgets/TabViewWidget.cpp" + "widgets/TabPageWidget.cpp" "webserver/WebServer.cpp" "webserver/StaticFileHandlers.cpp" "webserver/ConfigHandlers.cpp" diff --git a/main/WidgetConfig.hpp b/main/WidgetConfig.hpp index 5e0e980..946301f 100644 --- a/main/WidgetConfig.hpp +++ b/main/WidgetConfig.hpp @@ -15,6 +15,8 @@ enum class WidgetType : uint8_t { BUTTON = 1, LED = 2, ICON = 3, + TABVIEW = 4, + TABPAGE = 5, }; enum class IconPosition : uint8_t { diff --git a/main/widgets/TabPageWidget.cpp b/main/widgets/TabPageWidget.cpp new file mode 100644 index 0000000..c19bf08 --- /dev/null +++ b/main/widgets/TabPageWidget.cpp @@ -0,0 +1,43 @@ +#include "TabPageWidget.hpp" +#include "esp_log.h" + +static const char* TAG = "TabPageWidget"; + +TabPageWidget::TabPageWidget(const WidgetConfig& config) + : Widget(config) +{ +} + +TabPageWidget::~TabPageWidget() { +} + +lv_obj_t* TabPageWidget::create(lv_obj_t* parent) { + // Parent MUST be a TabView + if (!parent || !lv_obj_check_type(parent, &lv_tabview_class)) { + ESP_LOGE(TAG, "Parent of TabPage must be a TabView!"); + // Fallback: create a container so we don't crash + obj_ = lv_obj_create(parent ? parent : lv_scr_act()); + return obj_; + } + + obj_ = lv_tabview_add_tab(parent, config_.text); + + // Pages fill the parent, so x/y/w/h are ignored usually, + // but LVGL handles layout. + + ESP_LOGI(TAG, "Created TabPage '%s'", config_.text); + return obj_; +} + +void TabPageWidget::applyStyle() { + if (obj_ == nullptr) return; + + // Pages usually transparent, but we can style them + // Don't use applyCommonStyle fully as it sets pos/size which we don't want + + if (config_.bgOpacity > 0) { + lv_obj_set_style_bg_color(obj_, lv_color_make( + config_.bgColor.r, config_.bgColor.g, config_.bgColor.b), 0); + lv_obj_set_style_bg_opa(obj_, config_.bgOpacity, 0); + } +} diff --git a/main/widgets/TabPageWidget.hpp b/main/widgets/TabPageWidget.hpp new file mode 100644 index 0000000..d40533d --- /dev/null +++ b/main/widgets/TabPageWidget.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "Widget.hpp" + +class TabPageWidget : public Widget { +public: + explicit TabPageWidget(const WidgetConfig& config); + ~TabPageWidget() override; + + lv_obj_t* create(lv_obj_t* parent) override; + void applyStyle() override; +}; diff --git a/main/widgets/TabViewWidget.cpp b/main/widgets/TabViewWidget.cpp new file mode 100644 index 0000000..2236aae --- /dev/null +++ b/main/widgets/TabViewWidget.cpp @@ -0,0 +1,53 @@ +#include "TabViewWidget.hpp" +#include "../WidgetManager.hpp" +#include "esp_log.h" + +static const char* TAG = "TabViewWidget"; + +TabViewWidget::TabViewWidget(const WidgetConfig& config) + : Widget(config) +{ +} + +TabViewWidget::~TabViewWidget() { +} + +lv_obj_t* TabViewWidget::create(lv_obj_t* parent) { + // Determine tab position based on config + // We can reuse iconPosition property: 0=Top, 1=Bottom, 2=Left, 3=Right + lv_dir_t tabPos = LV_DIR_TOP; + if (config_.iconPosition == 1) tabPos = LV_DIR_BOTTOM; + else if (config_.iconPosition == 2) tabPos = LV_DIR_LEFT; + else if (config_.iconPosition == 3) tabPos = LV_DIR_RIGHT; + + // Use iconSize as tab height (e.g., 50px) + lv_coord_t tabHeight = config_.iconSize > 0 ? config_.iconSize * 10 : 50; + + // LVGL v9 API: Only parent as argument + obj_ = lv_tabview_create(parent); + lv_tabview_set_tab_bar_position(obj_, tabPos); + lv_tabview_set_tab_bar_size(obj_, tabHeight); + + lv_obj_set_pos(obj_, config_.x, config_.y); + lv_obj_set_size(obj_, config_.width > 0 ? config_.width : 300, + config_.height > 0 ? config_.height : 200); + + ESP_LOGI(TAG, "Created TabView at %d,%d", config_.x, config_.y); + return obj_; +} + +void TabViewWidget::applyStyle() { + if (obj_ == nullptr) return; + + // Apply styling to the main container + applyCommonStyle(); + + // Style the tab buttons (Tab Bar in v9) + lv_obj_t* bar = lv_tabview_get_tab_bar(obj_); + + lv_obj_set_style_bg_color(bar, lv_color_make( + config_.bgColor.r, config_.bgColor.g, config_.bgColor.b), 0); + + lv_obj_set_style_text_color(bar, lv_color_make( + config_.textColor.r, config_.textColor.g, config_.textColor.b), 0); +} diff --git a/main/widgets/TabViewWidget.hpp b/main/widgets/TabViewWidget.hpp new file mode 100644 index 0000000..0a6002c --- /dev/null +++ b/main/widgets/TabViewWidget.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "Widget.hpp" + +class TabViewWidget : public Widget { +public: + explicit TabViewWidget(const WidgetConfig& config); + ~TabViewWidget() override; + + lv_obj_t* create(lv_obj_t* parent) override; + void applyStyle() override; +}; diff --git a/main/widgets/WidgetFactory.cpp b/main/widgets/WidgetFactory.cpp index f4a2430..c000594 100644 --- a/main/widgets/WidgetFactory.cpp +++ b/main/widgets/WidgetFactory.cpp @@ -3,6 +3,8 @@ #include "ButtonWidget.hpp" #include "LedWidget.hpp" #include "IconWidget.hpp" +#include "TabViewWidget.hpp" +#include "TabPageWidget.hpp" std::unique_ptr WidgetFactory::create(const WidgetConfig& config) { if (!config.visible) return nullptr; @@ -16,6 +18,10 @@ std::unique_ptr WidgetFactory::create(const WidgetConfig& config) { return std::make_unique(config); case WidgetType::ICON: return std::make_unique(config); + case WidgetType::TABVIEW: + return std::make_unique(config); + case WidgetType::TABPAGE: + return std::make_unique(config); default: return nullptr; } diff --git a/web-interface/src/components/CanvasArea.vue b/web-interface/src/components/CanvasArea.vue index 872fea6..45f876d 100644 --- a/web-interface/src/components/CanvasArea.vue +++ b/web-interface/src/components/CanvasArea.vue @@ -15,8 +15,8 @@ :scale="store.canvasScale" :selected="store.selectedWidgetId === widget.id" @select="store.selectedWidgetId = widget.id" - @drag-start="startDrag(widget.id, $event)" - @resize-start="startResize(widget.id, $event)" + @drag-start="startDrag($event)" + @resize-start="startResize($event)" /> @@ -70,7 +70,8 @@ function snap(val) { let dragState = null; let resizeState = null; -function startDrag(id, e) { +function startDrag(payload) { + const { id, event: e } = payload; if (e.target.closest('.resize-handle')) return; e.preventDefault(); store.selectedWidgetId = id; @@ -123,8 +124,18 @@ function drag(e) { if (w.parentId !== -1) { const parent = store.activeScreen.widgets.find(p => p.id === w.parentId); if (parent) { - maxW = parent.w; - maxH = parent.h; + if (parent.type === WIDGET_TYPES.TABPAGE) { + // Parent is a TabPage (w=0, h=0), so look at grandparent (TabView) + const grandParent = store.activeScreen.widgets.find(gp => gp.id === parent.parentId); + if (grandParent) { + maxW = grandParent.w; + maxH = grandParent.h; + // Adjust for tab bar? For simplicity use full size for now + } + } else { + maxW = parent.w; + maxH = parent.h; + } } } @@ -141,7 +152,8 @@ function endDrag() { document.removeEventListener('touchend', endDrag); } -function startResize(id, e) { +function startResize(payload) { + const { id, event: e } = payload; e.preventDefault(); store.selectedWidgetId = id; diff --git a/web-interface/src/components/SidebarLeft.vue b/web-interface/src/components/SidebarLeft.vue index cbeb645..1ad1d7d 100644 --- a/web-interface/src/components/SidebarLeft.vue +++ b/web-interface/src/components/SidebarLeft.vue @@ -22,6 +22,10 @@ Icon Symbol + diff --git a/web-interface/src/components/SidebarRight.vue b/web-interface/src/components/SidebarRight.vue index 5006740..6c30d2a 100644 --- a/web-interface/src/components/SidebarRight.vue +++ b/web-interface/src/components/SidebarRight.vue @@ -93,6 +93,27 @@ + + + +