From 56953c9326354bbc23f8cd926ec0b322ddebc73f Mon Sep 17 00:00:00 2001 From: forntoh Date: Fri, 24 Apr 2026 09:51:34 +0200 Subject: [PATCH 1/2] feat: add graphical capabilities to core items and widgets --- .../overview/rendering/graphical-display.rst | 16 ++++ src/BaseItemManyWidgets.h | 96 +++++++++++++++++-- src/BaseItemZeroWidget.h | 12 ++- src/ItemBool.h | 42 +++++++- src/ItemLabel.h | 4 +- src/ItemSubMenu.h | 10 ++ src/ItemToggle.h | 32 ++++++- src/ItemValue.h | 12 ++- src/MenuItem.h | 21 +++- .../GraphicalValueSelectionRenderer.h | 16 ++++ src/widget/BaseWidget.h | 4 +- src/widget/WidgetBool.h | 9 +- src/widget/WidgetList.h | 5 +- src/widget/WidgetRange.h | 2 + test/ItemValue.cpp | 91 ++++++++++++++++++ 15 files changed, 351 insertions(+), 21 deletions(-) create mode 100644 src/renderer/GraphicalValueSelectionRenderer.h diff --git a/docs/source/overview/rendering/graphical-display.rst b/docs/source/overview/rendering/graphical-display.rst index 27c28170..434adec6 100644 --- a/docs/source/overview/rendering/graphical-display.rst +++ b/docs/source/overview/rendering/graphical-display.rst @@ -24,6 +24,22 @@ Menu integration through ``GraphicalRendererContext``. This lets the renderer recompute visible rows and value-column widths when fonts differ per item. +Item capabilities +----------------- + +Built-in items now expose graphical capabilities through +``GraphicalMenuItem`` without RTTI: + +- ``ITEM_BASIC`` and ``ITEM_LABEL`` opt into graphical-item capabilities. +- ``ITEM_BOOL`` and ``ITEM_TOGGLE`` expose toggle state for checkbox drawing. +- Widget-based items (for example ``ITEM_LIST`` and ``ITEM_RANGE``) expose + list-indicator support. +- ``ITEM_VALUE`` reports the rendered value width for right-aligned layout. + +Renderer-specific enhancements remain optional through ``queryExtension()``. +For example, indicators use ``GraphicalIndicatorRenderer`` and value selection +highlighting can be added with ``GraphicalValueSelectionRenderer``. + Basic usage ----------- diff --git a/src/BaseItemManyWidgets.h b/src/BaseItemManyWidgets.h index c9a3a5d4..020711c7 100644 --- a/src/BaseItemManyWidgets.h +++ b/src/BaseItemManyWidgets.h @@ -4,12 +4,16 @@ #include "LcdMenu.h" #include "MenuItem.h" +#include "display/GraphicalDisplayInterface.h" +#include "renderer/GraphicalIndicatorRenderer.h" +#include "renderer/GraphicalMenuItem.h" +#include "renderer/GraphicalValueSelectionRenderer.h" #include "utils/lcd_menu_utils.h" #include "utils/std.h" #include "widget/BaseWidget.h" #include -class BaseItemManyWidgets : public MenuItem { +class BaseItemManyWidgets : public MenuItem, public GraphicalMenuItem { protected: std::vector widgets; uint8_t activeWidget = 0; @@ -71,6 +75,38 @@ class BaseItemManyWidgets : public MenuItem { } } + uint8_t measureGraphicalValueWidth(GraphicalDisplayInterface* display) const override { + if (display == NULL) { + return 0; + } + + char buf[ITEM_DRAW_BUFFER_SIZE]; + uint8_t index = 0; + for (size_t i = 0; i < widgets.size() && index < ITEM_DRAW_BUFFER_SIZE - 1; i++) { + uint8_t written = widgets[i]->draw(buf, index); + uint8_t maxWritable = static_cast(ITEM_DRAW_BUFFER_SIZE - 1 - index); + index += written > maxWritable ? maxWritable : written; + } + buf[index] = '\0'; + return display->getTextWidth(buf); + } + + bool hasGraphicalListIndicator() const override { + for (size_t i = 0; i < widgets.size(); i++) { + if (widgets[i] != NULL && widgets[i]->isList()) { + return true; + } + } + return false; + } + + const void* queryCapability(uint8_t capabilityId) const override { + if (capabilityId == GraphicalMenuItem::capabilityId()) { + return static_cast(this); + } + return MenuItem::queryCapability(capabilityId); + } + virtual ~BaseItemManyWidgets() { for (auto widget : widgets) { delete widget; @@ -111,22 +147,68 @@ class BaseItemManyWidgets : public MenuItem { uint8_t index = 0; uint8_t cursorCol = 0; + bool hasListWidget = false; + uint8_t activeSegmentStart = 0; + uint8_t activeSegmentLength = 0; + + GraphicalValueSelectionRenderer* valueSelectionRenderer = + static_cast(renderer->queryExtension(GraphicalValueSelectionRenderer::extensionId())); for (uint8_t i = 0; i < widgets.size(); i++) { - index += widgets[i]->draw(buf, index); + uint8_t widgetStart = index; + uint8_t written = widgets[i]->draw(buf, index); + uint8_t maxWritable = index < ITEM_DRAW_BUFFER_SIZE - 1 ? static_cast(ITEM_DRAW_BUFFER_SIZE - 1 - index) : 0; + index += written > maxWritable ? maxWritable : written; + hasListWidget = hasListWidget || widgets[i]->isList(); if (i == activeWidget && MenuItem::isEditing()) { - // Calculate the available space for the widgets after the text - size_t v_size = renderer->getEffectiveCols() - strlen(text) - 1; - // Adjust the view shift to ensure the active widget is visible - renderer->viewShift = index > v_size ? index - v_size : 0; + activeSegmentStart = widgetStart; + activeSegmentLength = index > widgetStart ? static_cast(index - widgetStart) : 0; + + const char* label = text == NULL ? "" : text; + int16_t valueArea = static_cast(renderer->getEffectiveCols()) - static_cast(strlen(label)) - 1; + int16_t shift = static_cast(index) - valueArea; + renderer->viewShift = shift > 0 ? static_cast(shift) : 0; + + if (valueSelectionRenderer != NULL) { + if (activeSegmentLength > 0) { + valueSelectionRenderer->setValueSelection(activeSegmentStart, activeSegmentLength); + } else { + valueSelectionRenderer->clearValueSelection(); + } + } + // Draw the item with the renderer, indicating if it's the last widget renderer->drawItem(text, buf, i == widgets.size() - 1); // Calculate the cursor column position for the active widget - cursorCol = renderer->getCursorCol() - 1 - widgets[i]->cursorOffset; + uint8_t endCol = renderer->getCursorCol(); + uint8_t offset = static_cast(1 + widgets[i]->cursorOffset); + cursorCol = endCol > offset ? endCol - offset : 0; } } + buf[index < ITEM_DRAW_BUFFER_SIZE ? index : ITEM_DRAW_BUFFER_SIZE - 1] = '\0'; + + if (valueSelectionRenderer != NULL) { + if (MenuItem::isEditing() && activeSegmentLength > 0) { + valueSelectionRenderer->setValueSelection(activeSegmentStart, activeSegmentLength); + } else { + valueSelectionRenderer->clearValueSelection(); + } + } + renderer->drawItem(text, buf); + if (valueSelectionRenderer != NULL) { + valueSelectionRenderer->clearValueSelection(); + } + + if (hasListWidget) { + GraphicalIndicatorRenderer* indicatorRenderer = + static_cast(renderer->queryExtension(GraphicalIndicatorRenderer::extensionId())); + if (indicatorRenderer != NULL) { + indicatorRenderer->drawListIndicator(); + } + } + if (MenuItem::isEditing()) { renderer->moveCursor(cursorCol, renderer->getCursorRow()); } diff --git a/src/BaseItemZeroWidget.h b/src/BaseItemZeroWidget.h index 5ed17451..78e35580 100644 --- a/src/BaseItemZeroWidget.h +++ b/src/BaseItemZeroWidget.h @@ -3,6 +3,7 @@ #define BASE_ITEM_ZERO_WIDGET_H #include "MenuItem.h" +#include "renderer/GraphicalMenuItem.h" /** * @class BaseItemZeroWidget @@ -15,7 +16,7 @@ * @note This class is intended to be used as a base class for other menu items. * It should not be instantiated directly. */ -class BaseItemZeroWidget : public MenuItem { +class BaseItemZeroWidget : public MenuItem, public GraphicalMenuItem { public: virtual ~BaseItemZeroWidget() = default; /** @@ -25,6 +26,13 @@ class BaseItemZeroWidget : public MenuItem { */ explicit BaseItemZeroWidget(const char* text) : MenuItem(text) {} + const void* queryCapability(uint8_t capabilityId) const override { + if (capabilityId == GraphicalMenuItem::capabilityId()) { + return static_cast(this); + } + return MenuItem::queryCapability(capabilityId); + } + protected: virtual void handleCommit(LcdMenu* menu) = 0; @@ -42,4 +50,4 @@ class BaseItemZeroWidget : public MenuItem { } }; -#endif \ No newline at end of file +#endif diff --git a/src/ItemBool.h b/src/ItemBool.h index 7a240a76..27a0fde1 100644 --- a/src/ItemBool.h +++ b/src/ItemBool.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include /** @@ -15,6 +16,10 @@ */ template class ItemBool : public ItemWidget { + private: + const char* textOn = NULL; + const char* textOff = NULL; + public: virtual ~ItemBool() = default; @@ -26,7 +31,40 @@ class ItemBool : public ItemWidget { const char* format, const uint8_t cursorOffset, typename ItemWidget::CallbackType callback) - : ItemWidget(text, new WidgetBool(value, textOn, textOff, format, cursorOffset), callback) {} + : ItemWidget(text, new WidgetBool(value, textOn, textOff, format, cursorOffset), callback), + textOn(textOn), + textOff(textOff) {} + + uint8_t measureGraphicalValueWidth(GraphicalDisplayInterface* display) const override { + if (display == NULL) { + return 0; + } + uint8_t toggleWidth = display->getFontHeight(); + if (toggleWidth > 4) { + toggleWidth -= 4; + } + + if (toggleWidth < 3) { + toggleWidth = 3; + } + + uint8_t onWidth = display->getTextWidth(textOn == NULL ? "" : textOn); + uint8_t offWidth = display->getTextWidth(textOff == NULL ? "" : textOff); + uint8_t textWidth = onWidth > offWidth ? onWidth : offWidth; + + return toggleWidth > textWidth ? toggleWidth : textWidth; + } + + bool hasGraphicalToggle() const override { return true; } + + bool graphicalToggleState() const override { + BaseWidget* widget = this->getWidgetAt(0); + if (widget == nullptr) { + return false; + } + BaseWidgetValue* boolWidget = static_cast*>(widget); + return static_cast(boolWidget->getValue()); + } }; /** @@ -76,4 +114,4 @@ inline ItemBool>* ITEM_BOOL_REF( const char* format = "%s", const uint8_t cursorOffset = 0) { return new ItemBool>(text, Ref(value), textOn, textOff, format, cursorOffset, callback); -} \ No newline at end of file +} diff --git a/src/ItemLabel.h b/src/ItemLabel.h index 4e8368f5..25ff964a 100644 --- a/src/ItemLabel.h +++ b/src/ItemLabel.h @@ -6,9 +6,9 @@ * @class ItemLabel * @brief An unselectable menu item used for titles or separators. */ -class ItemLabel final : public MenuItem { +class ItemLabel final : public BasicItem { public: - explicit ItemLabel(const char* text) : MenuItem(text) {} + explicit ItemLabel(const char* text) : BasicItem(text) {} bool isSelectable() const override { return false; } }; diff --git a/src/ItemSubMenu.h b/src/ItemSubMenu.h index 33250fdc..4137ba5b 100644 --- a/src/ItemSubMenu.h +++ b/src/ItemSubMenu.h @@ -3,6 +3,7 @@ #include "BaseItemZeroWidget.h" #include "LcdMenu.h" #include "MenuScreen.h" +#include "renderer/GraphicalIndicatorRenderer.h" /** * @class ItemSubMenu @@ -34,6 +35,15 @@ class ItemSubMenu : public BaseItemZeroWidget { } protected: + void draw(MenuRenderer* renderer) override { + renderer->drawItem(text, nullptr); + GraphicalIndicatorRenderer* indicatorRenderer = + static_cast(renderer->queryExtension(GraphicalIndicatorRenderer::extensionId())); + if (indicatorRenderer != NULL) { + indicatorRenderer->drawSubMenuIndicator(); + } + } + void handleCommit(LcdMenu* menu) override { LOG(F("ItemSubMenu::changeScreen"), text); screen->setParent(menu->getScreen()); diff --git a/src/ItemToggle.h b/src/ItemToggle.h index f90d925c..290637b1 100644 --- a/src/ItemToggle.h +++ b/src/ItemToggle.h @@ -3,6 +3,8 @@ #include "LcdMenu.h" #include "MenuItem.h" +#include "display/GraphicalDisplayInterface.h" +#include "renderer/GraphicalMenuItem.h" #include /** @@ -16,7 +18,7 @@ * * Additionally to `text` this item has ON/OFF `enabled` state. */ -class ItemToggle : public MenuItem { +class ItemToggle : public MenuItem, public GraphicalMenuItem { private: bool enabled = false; const char* textOn = NULL; @@ -82,6 +84,34 @@ class ItemToggle : public MenuItem { const char* getTextOff() { return this->textOff; } + uint8_t measureGraphicalValueWidth(GraphicalDisplayInterface* display) const override { + if (display == NULL) { + return 0; + } + uint8_t toggleWidth = display->getFontHeight(); + if (toggleWidth > 4) { + toggleWidth -= 4; + } + if (toggleWidth < 3) { + toggleWidth = 3; + } + uint8_t onWidth = display->getTextWidth(textOn == NULL ? "" : textOn); + uint8_t offWidth = display->getTextWidth(textOff == NULL ? "" : textOff); + uint8_t textWidth = onWidth > offWidth ? onWidth : offWidth; + return toggleWidth > textWidth ? toggleWidth : textWidth; + } + + bool hasGraphicalToggle() const override { return true; } + + bool graphicalToggleState() const override { return enabled; } + + const void* queryCapability(uint8_t capabilityId) const override { + if (capabilityId == GraphicalMenuItem::capabilityId()) { + return static_cast(this); + } + return MenuItem::queryCapability(capabilityId); + } + void draw(MenuRenderer* renderer) override { renderer->drawItem(text, enabled ? textOn : textOff); }; diff --git a/src/ItemValue.h b/src/ItemValue.h index 85c82fd1..cc7bbfbf 100644 --- a/src/ItemValue.h +++ b/src/ItemValue.h @@ -1,5 +1,6 @@ #pragma once +#include "display/GraphicalDisplayInterface.h" #include "utils/custom_printf.h" #include "BaseItemZeroWidget.h" @@ -31,6 +32,15 @@ class ItemValue : public BaseItemZeroWidget { snprintf(buffer, ITEM_DRAW_BUFFER_SIZE, format, value); renderer->drawItem(text, buffer); } + + uint8_t measureGraphicalValueWidth(GraphicalDisplayInterface* display) const override { + if (display == NULL) { + return 0; + } + char buffer[ITEM_DRAW_BUFFER_SIZE]; + snprintf(buffer, ITEM_DRAW_BUFFER_SIZE, format, value); + return display->getTextWidth(buffer); + } }; /** @@ -54,4 +64,4 @@ inline MenuItem* ITEM_VALUE( T& value, const char* format = "%s") { return new ItemValue(text, value, format); -} \ No newline at end of file +} diff --git a/src/MenuItem.h b/src/MenuItem.h index d084a0f7..884e50dd 100644 --- a/src/MenuItem.h +++ b/src/MenuItem.h @@ -27,8 +27,9 @@ #ifndef MenuItem_H #define MenuItem_H -#define ITEM_DRAW_BUFFER_SIZE 25 +#define ITEM_DRAW_BUFFER_SIZE 64 +#include "renderer/GraphicalMenuItem.h" #include "renderer/MenuRenderer.h" #include "utils/lcd_menu_constants.h" #include @@ -109,7 +110,9 @@ class MenuItem { * Effectively const, but initialized lately when renderer is injected. */ inline uint8_t getViewSize(MenuRenderer* renderer) const { - return renderer->getEffectiveCols() - strlen(text) - 1 + renderer->viewShift; + const char* label = text == NULL ? "" : text; + int16_t viewSize = static_cast(renderer->getEffectiveCols()) - static_cast(strlen(label)) - 1 + renderer->viewShift; + return viewSize > 0 ? static_cast(viewSize) : 0; }; /** * @brief Process a command decoded in 1 byte. @@ -134,6 +137,18 @@ class MenuItem { }; }; -#define ITEM_BASIC(...) (new MenuItem(__VA_ARGS__)) +class BasicItem : public MenuItem, public GraphicalMenuItem { + public: + explicit BasicItem(const char* text) : MenuItem(text) {} + + const void* queryCapability(uint8_t capabilityId) const override { + if (capabilityId == GraphicalMenuItem::capabilityId()) { + return static_cast(this); + } + return MenuItem::queryCapability(capabilityId); + } +}; + +#define ITEM_BASIC(...) (new BasicItem(__VA_ARGS__)) #endif diff --git a/src/renderer/GraphicalValueSelectionRenderer.h b/src/renderer/GraphicalValueSelectionRenderer.h new file mode 100644 index 00000000..be66c14b --- /dev/null +++ b/src/renderer/GraphicalValueSelectionRenderer.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +/** + * @brief Optional renderer interface for highlighting a value substring. + */ +class GraphicalValueSelectionRenderer { + public: + static uint8_t extensionId() { return 4; } + + virtual ~GraphicalValueSelectionRenderer() {} + + virtual void setValueSelection(uint8_t start, uint8_t length) = 0; + virtual void clearValueSelection() = 0; +}; diff --git a/src/widget/BaseWidget.h b/src/widget/BaseWidget.h index 6ac5fb99..17beb0fb 100644 --- a/src/widget/BaseWidget.h +++ b/src/widget/BaseWidget.h @@ -47,10 +47,12 @@ class BaseWidget { */ virtual uint8_t draw(char* buffer, const uint8_t start = 0) = 0; + virtual bool isList() const { return false; } + virtual void startEdit() {} virtual void cancelEdit() {} public: virtual ~BaseWidget() = default; -}; \ No newline at end of file +}; diff --git a/src/widget/WidgetBool.h b/src/widget/WidgetBool.h index 7b11ad06..719e3e74 100644 --- a/src/widget/WidgetBool.h +++ b/src/widget/WidgetBool.h @@ -34,7 +34,14 @@ class WidgetBool : public BaseWidgetValue { protected: uint8_t draw(char* buffer, const uint8_t start) override { if (start >= ITEM_DRAW_BUFFER_SIZE) return 0; - return snprintf(buffer + start, ITEM_DRAW_BUFFER_SIZE - start, this->format, static_cast(this->value) ? textOn : textOff); + + const char* selectedText = static_cast(this->value) ? textOn : textOff; + if (selectedText == NULL) { + selectedText = ""; + } + + const char* format = this->format == NULL ? "%s" : this->format; + return snprintf(buffer + start, ITEM_DRAW_BUFFER_SIZE - start, format, selectedText); } /** * @brief Process command. diff --git a/src/widget/WidgetList.h b/src/widget/WidgetList.h index 89250593..403f85d1 100644 --- a/src/widget/WidgetList.h +++ b/src/widget/WidgetList.h @@ -61,6 +61,9 @@ class WidgetList : public BaseWidgetValue { return false; } } + + bool isList() const override { return true; } + void updateValue(const __FlashStringHelper* action) { BaseWidgetValue::handleChange(); LOG(action, (uint8_t)this->value); @@ -146,4 +149,4 @@ inline BaseWidgetValue>* WIDGET_LIST_REF( const bool cycle = false, void (*callback)(const Ref&) = nullptr) { return new WidgetList>(values, Ref(activePosition), format, cursorOffset, cycle, callback); -} \ No newline at end of file +} diff --git a/src/widget/WidgetRange.h b/src/widget/WidgetRange.h index 84e2ea4e..accce4ff 100644 --- a/src/widget/WidgetRange.h +++ b/src/widget/WidgetRange.h @@ -108,6 +108,8 @@ class WidgetRange : public BaseWidgetValue { return snprintf(buffer + start, ITEM_DRAW_BUFFER_SIZE - start, this->format, static_cast(this->value)); } + bool isList() const override { return true; } + void startEdit() override { originalValue = static_cast(this->value); } void cancelEdit() override { this->value = originalValue; } diff --git a/test/ItemValue.cpp b/test/ItemValue.cpp index c1d51f53..4ba0611b 100644 --- a/test/ItemValue.cpp +++ b/test/ItemValue.cpp @@ -1,10 +1,18 @@ #include "Godmode.h" #include +#include +#include +#include +#include +#include #include #include #include #include +#include +#include #include +#include #define LCD_ROWS 1 #define LCD_COLS 16 @@ -40,6 +48,33 @@ class CaptureRenderer : public MenuRenderer { uint8_t getEffectiveCols() const override { return maxCols; } }; +class GraphicalMeasureDisplay : public GraphicalDisplayInterface { + public: + static const uint8_t kCharWidth = 6; + void begin() override {} + void clear() override {} + void show() override {} + void hide() override {} + void draw(uint8_t) override {} + void draw(const char*) override {} + void setCursor(uint8_t, uint8_t) override {} + void setBacklight(bool) override {} + void setFont(const uint8_t*) override {} + uint8_t getDisplayWidth() const override { return 128; } + uint8_t getDisplayHeight() const override { return 64; } + uint8_t getFontWidth() const override { return kCharWidth; } + uint8_t getFontHeight() const override { return 8; } + uint8_t getTextWidth(const char* text) override { + return text == NULL ? 0 : static_cast(strlen(text) * kCharWidth); + } + void setDrawColor(uint8_t) override {} + void clearBuffer() override {} + void sendBuffer() override {} + void drawBox(uint8_t, uint8_t, uint8_t, uint8_t) override {} + void drawFrame(uint8_t, uint8_t, uint8_t, uint8_t) override {} + void drawXbm(uint8_t, uint8_t, uint8_t, uint8_t, const uint8_t*) override {} +}; + // clang-format off float tracked = 0.0; MENU_SCREEN(mainScreen, mainItems, ITEM_VALUE("Temp", tracked, "%.1f")); @@ -60,4 +95,60 @@ unittest(item_value_updates_after_poll) { assertEqual("42.5", renderer.lastValue.c_str()); } +unittest(basic_and_label_expose_graphical_capability) { + MenuItem* basic = ITEM_BASIC("Basic"); + ItemLabel* label = ITEM_LABEL("Label"); + + assertTrue(basic->queryCapability(GraphicalMenuItem::capabilityId()) != NULL); + assertTrue(label->queryCapability(GraphicalMenuItem::capabilityId()) != NULL); + + delete basic; + delete label; +} + +unittest(toggle_and_bool_report_graphical_toggle_state) { + ItemToggle toggle("Power", "ON", "OFF", nullptr); + const GraphicalMenuItem* toggleCapability = static_cast(toggle.queryCapability(GraphicalMenuItem::capabilityId())); + assertTrue(toggleCapability != NULL); + assertTrue(toggleCapability->hasGraphicalToggle()); + assertFalse(toggleCapability->graphicalToggleState()); + + toggle.setIsOn(true); + assertTrue(toggleCapability->graphicalToggleState()); + + ItemBool boolItem("Enabled", true, "YES", "NO", "%s", 0, nullptr); + const GraphicalMenuItem* boolCapability = static_cast(boolItem.queryCapability(GraphicalMenuItem::capabilityId())); + assertTrue(boolCapability != NULL); + assertTrue(boolCapability->hasGraphicalToggle()); + assertTrue(boolCapability->graphicalToggleState()); +} + +unittest(value_and_widget_items_measure_graphical_width) { + GraphicalMeasureDisplay display; + + int value = 123; + ItemValue valueItem("Count", value, "%d"); + assertEqual((uint8_t)(3 * GraphicalMeasureDisplay::kCharWidth), valueItem.measureGraphicalValueWidth(&display)); + + ItemBool boolItem("Enabled", false, "YES", "NO", "%s", 0, nullptr); + assertEqual((uint8_t)(3 * GraphicalMeasureDisplay::kCharWidth), boolItem.measureGraphicalValueWidth(&display)); +} + +unittest(list_and_range_items_report_graphical_list_indicator) { + std::vector values = {"A", "B", "C"}; + ItemList* listItem = ITEM_LIST("Mode", values, nullptr); + ItemRange* rangeItem = ITEM_RANGE("Speed", 3, 1, 0, 9, nullptr, "%d"); + + const GraphicalMenuItem* listCapability = static_cast(listItem->queryCapability(GraphicalMenuItem::capabilityId())); + const GraphicalMenuItem* rangeCapability = static_cast(rangeItem->queryCapability(GraphicalMenuItem::capabilityId())); + + assertTrue(listCapability != NULL); + assertTrue(rangeCapability != NULL); + assertTrue(listCapability->hasGraphicalListIndicator()); + assertTrue(rangeCapability->hasGraphicalListIndicator()); + + delete listItem; + delete rangeItem; +} + unittest_main() From 21b03d02ed53966f760a20684ec0adca8c59b18e Mon Sep 17 00:00:00 2001 From: forntoh Date: Fri, 24 Apr 2026 10:12:07 +0200 Subject: [PATCH 2/2] test: fix graphical capability assertions in item tests --- test/ItemValue.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/ItemValue.cpp b/test/ItemValue.cpp index 4ba0611b..742a1647 100644 --- a/test/ItemValue.cpp +++ b/test/ItemValue.cpp @@ -128,16 +128,21 @@ unittest(value_and_widget_items_measure_graphical_width) { int value = 123; ItemValue valueItem("Count", value, "%d"); - assertEqual((uint8_t)(3 * GraphicalMeasureDisplay::kCharWidth), valueItem.measureGraphicalValueWidth(&display)); + const GraphicalMenuItem* valueCapability = static_cast(valueItem.queryCapability(GraphicalMenuItem::capabilityId())); + assertTrue(valueCapability != NULL); + assertEqual((uint8_t)(3 * GraphicalMeasureDisplay::kCharWidth), valueCapability->measureGraphicalValueWidth(&display)); ItemBool boolItem("Enabled", false, "YES", "NO", "%s", 0, nullptr); - assertEqual((uint8_t)(3 * GraphicalMeasureDisplay::kCharWidth), boolItem.measureGraphicalValueWidth(&display)); + const GraphicalMenuItem* boolCapability = static_cast(boolItem.queryCapability(GraphicalMenuItem::capabilityId())); + assertTrue(boolCapability != NULL); + assertEqual((uint8_t)(3 * GraphicalMeasureDisplay::kCharWidth), boolCapability->measureGraphicalValueWidth(&display)); } unittest(list_and_range_items_report_graphical_list_indicator) { std::vector values = {"A", "B", "C"}; ItemList* listItem = ITEM_LIST("Mode", values, nullptr); - ItemRange* rangeItem = ITEM_RANGE("Speed", 3, 1, 0, 9, nullptr, "%d"); + void (*rangeCallback)(const int) = nullptr; + ItemRange* rangeItem = ITEM_RANGE("Speed", 3, 1, 0, 9, rangeCallback, "%d"); const GraphicalMenuItem* listCapability = static_cast(listItem->queryCapability(GraphicalMenuItem::capabilityId())); const GraphicalMenuItem* rangeCapability = static_cast(rangeItem->queryCapability(GraphicalMenuItem::capabilityId()));