From ceb46348ee77e8fcc31a963f3d497316094dcea5 Mon Sep 17 00:00:00 2001 From: forntoh Date: Sat, 25 Apr 2026 10:44:34 +0200 Subject: [PATCH] feat: expose graphical value selection renderer hook --- .../overview/rendering/graphical-display.rst | 4 + src/renderer/GraphicalDisplayRenderer.cpp | 91 ++++++++++++++++++- src/renderer/GraphicalDisplayRenderer.h | 7 ++ test/GraphicalDisplayRenderer.cpp | 52 +++++++++++ 4 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 test/GraphicalDisplayRenderer.cpp diff --git a/docs/source/overview/rendering/graphical-display.rst b/docs/source/overview/rendering/graphical-display.rst index 434adec6..30fb0a94 100644 --- a/docs/source/overview/rendering/graphical-display.rst +++ b/docs/source/overview/rendering/graphical-display.rst @@ -40,6 +40,10 @@ Renderer-specific enhancements remain optional through ``queryExtension()``. For example, indicators use ``GraphicalIndicatorRenderer`` and value selection highlighting can be added with ``GraphicalValueSelectionRenderer``. +``GraphicalDisplayRenderer`` now exposes ``GraphicalValueSelectionRenderer`` so +``ItemInput`` and ``ItemInputCharset`` can render active-character selection on +graphical displays. + Basic usage ----------- diff --git a/src/renderer/GraphicalDisplayRenderer.cpp b/src/renderer/GraphicalDisplayRenderer.cpp index fbe05814..97c31fe2 100644 --- a/src/renderer/GraphicalDisplayRenderer.cpp +++ b/src/renderer/GraphicalDisplayRenderer.cpp @@ -7,6 +7,30 @@ namespace { const uint8_t listGlyph[] = {0x08, 0x1C, 0x3E, 0x00, 0x3E, 0x1C, 0x08, 0x00}; +const uint8_t textBufferSize = ITEM_DRAW_BUFFER_SIZE; + +uint8_t safeLength(const char* text) { + if (text == NULL) { + return 0; + } + size_t len = strlen(text); + return len > 255 ? 255 : static_cast(len); +} + +void copyTextRange(const char* text, uint8_t start, uint8_t count, char* out) { + if (text == NULL || count == 0) { + out[0] = '\0'; + return; + } + + uint8_t i = 0; + while (text[start] != '\0' && i < count && i < textBufferSize - 1) { + out[i] = text[start]; + i++; + start++; + } + out[i] = '\0'; +} const GraphicalMenuItem* asGraphical(const MenuItem* item) { if (item == NULL) { @@ -100,6 +124,7 @@ void GraphicalDisplayRenderer::setValueAreaWidth(uint8_t width) { void GraphicalDisplayRenderer::setActiveItem(const MenuItem* item) { activeItem = item; + clearValueSelection(); applyItemFont(item); } @@ -107,6 +132,18 @@ GraphicalDisplayInterface* GraphicalDisplayRenderer::getGraphicalDisplay() { return gDisplay; } +void GraphicalDisplayRenderer::setValueSelection(uint8_t start, uint8_t length) { + valueSelectionStart = start; + valueSelectionLength = length; + hasValueSelection = length > 0; +} + +void GraphicalDisplayRenderer::clearValueSelection() { + valueSelectionStart = 0; + valueSelectionLength = 0; + hasValueSelection = false; +} + void* GraphicalDisplayRenderer::queryExtension(uint8_t extensionId) { if (extensionId == FrameLifecycleRenderer::extensionId()) { return static_cast(this); @@ -114,6 +151,9 @@ void* GraphicalDisplayRenderer::queryExtension(uint8_t extensionId) { if (extensionId == GraphicalIndicatorRenderer::extensionId()) { return static_cast(this); } + if (extensionId == GraphicalValueSelectionRenderer::extensionId()) { + return static_cast(this); + } if (extensionId == GraphicalRendererContext::extensionId()) { return static_cast(this); } @@ -127,6 +167,9 @@ const void* GraphicalDisplayRenderer::queryExtension(uint8_t extensionId) const if (extensionId == GraphicalIndicatorRenderer::extensionId()) { return static_cast(this); } + if (extensionId == GraphicalValueSelectionRenderer::extensionId()) { + return static_cast(this); + } if (extensionId == GraphicalRendererContext::extensionId()) { return static_cast(this); } @@ -174,6 +217,9 @@ void GraphicalDisplayRenderer::drawItem(const char* text, const char* value, boo gDisplay->draw(label); x += measureText(label) + 1; + const GraphicalMenuItem* graphicalItem = asGraphical(activeItem); + bool tightSelection = graphicalItem != NULL && graphicalItem->useTightGraphicalSelectionBox(); + if (value != NULL && value[0] != '\0') { uint8_t valueWidth = valueAreaWidth; if (valueWidth == 0) { @@ -183,8 +229,51 @@ void GraphicalDisplayRenderer::drawItem(const char* text, const char* value, boo uint8_t valueX = valueRight > valueWidth ? valueRight - valueWidth : x; gDisplay->setCursor(valueX, baseline); gDisplay->draw(value); + + if (hasFocus && MenuItem::isEditing() && hasValueSelection) { + uint8_t valueLen = safeLength(value); + uint8_t selectionStart = valueSelectionStart > valueLen ? valueLen : valueSelectionStart; + uint16_t rawEnd = static_cast(valueSelectionStart) + valueSelectionLength; + uint8_t selectionEnd = rawEnd > valueLen ? valueLen : static_cast(rawEnd); + + if (selectionEnd <= selectionStart && selectionStart < valueLen) { + selectionEnd = selectionStart + 1; + } + + if (selectionEnd > selectionStart) { + char prefixBuf[textBufferSize]; + char selectedBuf[textBufferSize]; + copyTextRange(value, 0, selectionStart, prefixBuf); + copyTextRange(value, selectionStart, static_cast(selectionEnd - selectionStart), selectedBuf); + + uint8_t prefixWidth = measureText(prefixBuf); + uint8_t selectedWidth = measureText(selectedBuf); + if (selectedWidth == 0) { + selectedWidth = gDisplay->getFontWidth() == 0 ? 1 : gDisplay->getFontWidth(); + } + + uint8_t selectedX = valueX + prefixWidth; + uint8_t pad = tightSelection ? 0 : 1; + uint8_t highlightX = selectedX > pad ? static_cast(selectedX - pad) : 0; + uint16_t highlightRight = static_cast(selectedX) + selectedWidth + pad; + if (highlightRight > valueRight) { + highlightRight = valueRight; + } + + uint8_t highlightWidth = highlightRight > highlightX + ? static_cast(highlightRight - highlightX) + : 0; + if (highlightWidth > 0) { + gDisplay->setDrawColor(1); + gDisplay->drawBox(highlightX, yTop, highlightWidth, h); + gDisplay->setDrawColor(0); + gDisplay->setCursor(selectedX, baseline); + gDisplay->draw(selectedBuf); + gDisplay->setDrawColor(1); + } + } + } } else { - const GraphicalMenuItem* graphicalItem = asGraphical(activeItem); if (graphicalItem != NULL && graphicalItem->hasGraphicalToggle()) { uint8_t box = toggleIndicatorWidth(); uint8_t xBox = contentRight > rightPadding + box ? contentRight - rightPadding - box : x; diff --git a/src/renderer/GraphicalDisplayRenderer.h b/src/renderer/GraphicalDisplayRenderer.h index 061323e7..37faf3c9 100644 --- a/src/renderer/GraphicalDisplayRenderer.h +++ b/src/renderer/GraphicalDisplayRenderer.h @@ -4,6 +4,7 @@ #include "GraphicalIndicatorRenderer.h" #include "GraphicalItemFont.h" #include "GraphicalRendererContext.h" +#include "GraphicalValueSelectionRenderer.h" #include "MenuRenderer.h" #include "display/GraphicalDisplayInterface.h" #include "utils/std.h" @@ -15,6 +16,7 @@ class GraphicalDisplayRenderer : public MenuRenderer, public FrameLifecycleRenderer, public GraphicalIndicatorRenderer, + public GraphicalValueSelectionRenderer, public GraphicalRendererContext { private: GraphicalDisplayInterface* gDisplay; @@ -29,6 +31,9 @@ class GraphicalDisplayRenderer : public MenuRenderer, uint8_t cursorPixelY = 0; uint8_t maxRowHeight = 8; uint8_t maxFontWidth = 1; + bool hasValueSelection = false; + uint8_t valueSelectionStart = 0; + uint8_t valueSelectionLength = 0; const char* cursorIcon; const char* editCursorIcon; @@ -74,6 +79,8 @@ class GraphicalDisplayRenderer : public MenuRenderer, void setValueAreaWidth(uint8_t width) override; void setActiveItem(const MenuItem* item) override; GraphicalDisplayInterface* getGraphicalDisplay() override; + void setValueSelection(uint8_t start, uint8_t length) override; + void clearValueSelection() override; void* queryExtension(uint8_t extensionId) override; const void* queryExtension(uint8_t extensionId) const override; diff --git a/test/GraphicalDisplayRenderer.cpp b/test/GraphicalDisplayRenderer.cpp new file mode 100644 index 00000000..9618fb87 --- /dev/null +++ b/test/GraphicalDisplayRenderer.cpp @@ -0,0 +1,52 @@ +#include +#include +#include +#include + +class StubGraphicalDisplay : public GraphicalDisplayInterface { + public: + 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 6; } + uint8_t getFontHeight() const override { return 8; } + uint8_t getTextWidth(const char* text) override { + if (text == NULL) { + return 0; + } + uint8_t len = 0; + while (text[len] != '\0') { + len++; + } + return static_cast(len * 6); + } + 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 {} +}; + +unittest(graphical_renderer_exposes_value_selection_extension) { + StubGraphicalDisplay display; + GraphicalDisplayRenderer renderer(&display); + + void* extension = renderer.queryExtension(GraphicalValueSelectionRenderer::extensionId()); + assertTrue(extension != NULL); + + GraphicalValueSelectionRenderer* selection = static_cast(extension); + selection->setValueSelection(1, 2); + selection->clearValueSelection(); +} + +unittest_main()