From 7f33b6c7ddde70d33f3d95c8b1e34a89e271fdd2 Mon Sep 17 00:00:00 2001 From: forntoh Date: Tue, 26 May 2026 19:24:12 +0200 Subject: [PATCH 1/2] feat: complete graphical renderer parity --- src/renderer/GraphicalDisplayRenderer.cpp | 442 ++++++++++++++++------ src/renderer/GraphicalDisplayRenderer.h | 4 +- test/GraphicalDisplayRenderer.cpp | 105 +++-- 3 files changed, 404 insertions(+), 147 deletions(-) diff --git a/src/renderer/GraphicalDisplayRenderer.cpp b/src/renderer/GraphicalDisplayRenderer.cpp index da170a58..97a333d1 100644 --- a/src/renderer/GraphicalDisplayRenderer.cpp +++ b/src/renderer/GraphicalDisplayRenderer.cpp @@ -6,8 +6,17 @@ #include namespace { -const uint8_t listGlyph[] = {0x08, 0x1C, 0x3E, 0x00, 0x3E, 0x1C, 0x08, 0x00}; -const uint8_t textBufferSize = ITEM_DRAW_BUFFER_SIZE; +static const uint8_t updownGlyph[] = { + 0x08, + 0x1C, + 0x3E, + 0x00, + 0x3E, + 0x1C, + 0x08, +}; + +static const uint8_t textBufferSize = 64; uint8_t safeLength(const char* text) { if (text == NULL) { @@ -17,6 +26,41 @@ uint8_t safeLength(const char* text) { return len > 255 ? 255 : static_cast(len); } +void copyTextWindow(const char* text, uint8_t maxChars, char* out) { + if (text == NULL || maxChars == 0) { + out[0] = '\0'; + return; + } + + uint8_t i = 0; + while (text[i] != '\0' && i < maxChars && i < textBufferSize - 1) { + out[i] = text[i]; + i++; + } + out[i] = '\0'; +} + +void copyTextWindowByWidth(const char* text, uint8_t maxPixelWidth, GraphicalDisplayInterface* display, char* out) { + if (text == NULL || display == NULL || maxPixelWidth == 0) { + out[0] = '\0'; + return; + } + + uint8_t i = 0; + while (text[i] != '\0' && i < textBufferSize - 1) { + out[i] = text[i]; + out[i + 1] = '\0'; + + if (display->getTextWidth(out) > maxPixelWidth) { + out[i] = '\0'; + return; + } + + i++; + } + out[i] = '\0'; +} + void copyTextRange(const char* text, uint8_t start, uint8_t count, char* out) { if (text == NULL || count == 0) { out[0] = '\0'; @@ -32,7 +76,7 @@ void copyTextRange(const char* text, uint8_t start, uint8_t count, char* out) { out[i] = '\0'; } -const GraphicalMenuItem* asGraphical(const MenuItem* item) { +const GraphicalMenuItem* toGraphicalMenuItem(const MenuItem* item) { if (item == NULL) { return NULL; } @@ -59,7 +103,6 @@ void GraphicalDisplayRenderer::setDefaultFont(const uint8_t* font) { gDisplay->setFont(defaultFont); } captureCurrentFontMetrics(); - applyItemFont(activeItem); } @@ -78,33 +121,35 @@ bool GraphicalDisplayRenderer::setItemFont(MenuItem* item, const uint8_t* font) } void GraphicalDisplayRenderer::captureCurrentFontMetrics() { - uint8_t h = gDisplay->getFontHeight(); - uint8_t w = gDisplay->getFontWidth(); - - if (h == 0) { - h = 8; + uint8_t currentHeight = gDisplay->getFontHeight(); + if (currentHeight == 0) { + currentHeight = 8; } - if (w == 0) { - w = 1; + + uint8_t currentWidth = gDisplay->getFontWidth(); + if (currentWidth == 0) { + currentWidth = 1; } - if (h > maxRowHeight) { - maxRowHeight = h; + if (currentHeight > maxRowHeight) { + maxRowHeight = currentHeight; } - if (w > maxFontWidth) { - maxFontWidth = w; + if (currentWidth > maxFontWidth) { + maxFontWidth = currentWidth; } } void GraphicalDisplayRenderer::applyItemFont(const MenuItem* item) { - const uint8_t* font = defaultFont; - const GraphicalMenuItem* graphicalItem = asGraphical(item); + const uint8_t* selectedFont = defaultFont; + const GraphicalMenuItem* graphicalItem = toGraphicalMenuItem(item); if (graphicalItem != NULL && graphicalItem->getGraphicalFont() != NULL) { - font = graphicalItem->getGraphicalFont(); + selectedFont = graphicalItem->getGraphicalFont(); } - if (font != NULL) { - gDisplay->setFont(font); + + if (selectedFont != NULL) { + gDisplay->setFont(selectedFont); } + captureCurrentFontMetrics(); } @@ -202,113 +247,260 @@ void GraphicalDisplayRenderer::draw(uint8_t byte) { void GraphicalDisplayRenderer::drawItem(const char* text, const char* value, bool padWithBlanks) { (void)padWithBlanks; - uint8_t h = rowHeight(); - uint8_t yTop = cursorRow * h; + uint8_t rowH = rowHeight(); + uint8_t displayWidth = gDisplay->getDisplayWidth(); + uint8_t top = cursorRow * rowH; + uint8_t fontHeight = gDisplay->getFontHeight(); - uint8_t baseline = yTop + (h > fontHeight ? (h + fontHeight) / 2 - 1 : h - 1); + if (fontHeight == 0 || fontHeight > rowH) { + fontHeight = rowH; + } + uint8_t baseline = top + (rowH - fontHeight) / 2 + fontHeight - 1; - uint8_t displayWidth = gDisplay->getDisplayWidth(); - uint8_t rightInset = totalItems > getMaxRows() ? scrollbarWidth + scrollbarGap : 0; + bool showScrollBar = totalItems > getMaxRows(); + uint8_t rightInset = showScrollBar ? scrollbarWidth + scrollbarGap : 0; uint8_t contentRight = displayWidth > rightInset ? displayWidth - rightInset : displayWidth; gDisplay->setDrawColor(0); - gDisplay->drawBox(0, yTop, contentRight, h); + if (showScrollBar) { + gDisplay->drawBox(0, top, contentRight, rowH); + } else { + gDisplay->drawBox(0, top, displayWidth, rowH); + } - if (hasFocus && !MenuItem::isEditing()) { + bool editing = MenuItem::isEditing(); + bool highlightRow = hasFocus && !editing; + if (highlightRow) { gDisplay->setDrawColor(1); - gDisplay->drawBox(0, yTop, contentRight, h); + gDisplay->drawBox(0, top, contentRight, rowH); gDisplay->setDrawColor(0); } else { gDisplay->setDrawColor(1); } - uint8_t x = leftPadding; - const char* focusedIcon = MenuItem::isEditing() ? editCursorIcon : cursorIcon; - uint8_t iconWidth = measureText(focusedIcon); - if (hasFocus && focusedIcon != NULL && focusedIcon[0] != '\0') { - gDisplay->setCursor(x, baseline); - gDisplay->draw(focusedIcon); + uint8_t charW = gDisplay->getFontWidth() == 0 ? 1 : gDisplay->getFontWidth(); + const char* focusedCursorIcon = editing ? editCursorIcon : cursorIcon; + uint8_t cursorAreaWidth = measureText(focusedCursorIcon); + uint8_t textX = leftPadding; + + if (cursorAreaWidth > 0) { + gDisplay->setCursor(textX, baseline); + if (hasFocus) { + gDisplay->draw(focusedCursorIcon); + } else { + gDisplay->draw(" "); + } + textX += cursorAreaWidth + cursorGap; + } + + const char* labelText = text == NULL ? "" : text; + uint8_t labelLen = safeLength(labelText); + + const GraphicalMenuItem* graphicalItem = toGraphicalMenuItem(activeItem); + bool hasToggle = graphicalItem != NULL && graphicalItem->hasGraphicalToggle(); + bool useToggleBox = hasToggle && (value == NULL || value[0] == '\0'); + bool hasValue = value != NULL || hasToggle; + bool hasListIndicator = graphicalItem != NULL && graphicalItem->hasGraphicalListIndicator(); + bool tightSelectionBox = graphicalItem != NULL && graphicalItem->useTightGraphicalSelectionBox(); + bool widgetEditingSelection = hasFocus && editing && hasValueSelection && value != NULL && !useToggleBox; + + uint8_t valueRight = contentRight > rightPadding ? contentRight - rightPadding : contentRight; + uint8_t valueLeft = valueRight; + uint8_t reservedForIndicator = hasListIndicator ? static_cast(listGlyphWidth + listGap) : 0; + if (valueRight > reservedForIndicator) { + valueRight -= reservedForIndicator; + } + + uint8_t alignedValueWidth = valueAreaWidth; + if (useToggleBox) { + alignedValueWidth = toggleIndicatorWidth(); + } else if (value != NULL && alignedValueWidth == 0) { + alignedValueWidth = measureText(value); + } + + uint8_t maxValueWidth = valueRight > textX ? valueRight - textX : 0; + if (alignedValueWidth > maxValueWidth) { + alignedValueWidth = maxValueWidth; + } + + if (hasValue && alignedValueWidth > 0 && valueRight > alignedValueWidth) { + valueLeft = valueRight - alignedValueWidth; } - x += iconWidth + cursorGap; - const char* label = text == NULL ? "" : text; - gDisplay->setCursor(x, baseline); - gDisplay->draw(label); - x += measureText(label) + 1; + uint8_t labelRight = !hasValue ? valueRight : (valueLeft > 1 ? valueLeft - 1 : valueLeft); - const GraphicalMenuItem* graphicalItem = asGraphical(activeItem); - bool tightSelection = graphicalItem != NULL && graphicalItem->useTightGraphicalSelectionBox(); + const char* labelPtr = labelText; + if (hasFocus && !widgetEditingSelection) { + labelPtr = viewShift < labelLen ? labelText + viewShift : ""; + } + + uint8_t labelPixelBudget = labelRight > textX ? labelRight - textX : 0; + char labelBuf[textBufferSize]; + copyTextWindowByWidth(labelPtr, labelPixelBudget, gDisplay, labelBuf); + + uint8_t drawnLabelWidth = measureText(labelBuf); + if (labelBuf[0] != '\0') { + gDisplay->setCursor(textX, baseline); + gDisplay->draw(labelBuf); + } - if (value != NULL && value[0] != '\0') { - uint8_t valueWidth = valueAreaWidth; - if (valueWidth == 0) { - valueWidth = measureText(value); + if (hasFocus && editing && hasValue && !useToggleBox) { + uint16_t desiredValueLeft = static_cast(textX) + drawnLabelWidth + 1; + if (desiredValueLeft < valueLeft) { + uint8_t extra = static_cast(valueLeft - desiredValueLeft); + uint16_t expandedWidth = static_cast(alignedValueWidth) + extra; + alignedValueWidth = expandedWidth > maxValueWidth ? maxValueWidth : static_cast(expandedWidth); + valueLeft = valueRight > alignedValueWidth ? static_cast(valueRight - alignedValueWidth) : valueLeft; } - uint8_t valueRight = contentRight > rightPadding ? contentRight - rightPadding : contentRight; - 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); + uint8_t drawnValueWidth = 0; + uint8_t valueX = valueLeft; + uint8_t valueShift = 0; - if (selectionEnd <= selectionStart && selectionStart < valueLen) { - selectionEnd = selectionStart + 1; + if (hasValue && alignedValueWidth > 0) { + if (useToggleBox) { + uint8_t boxSize = toggleIndicatorWidth(); + if (boxSize > alignedValueWidth) { + boxSize = alignedValueWidth; } - if (selectionEnd > selectionStart) { - char prefixBuf[textBufferSize]; - char selectedBuf[textBufferSize]; - copyTextRange(value, 0, selectionStart, prefixBuf); - copyTextRange(value, selectionStart, static_cast(selectionEnd - selectionStart), selectedBuf); + valueX = valueRight > boxSize ? valueRight - boxSize : valueLeft; + uint8_t boxY = top + (rowH > boxSize ? (rowH - boxSize) / 2 : 0); - uint8_t prefixWidth = measureText(prefixBuf); - uint8_t selectedWidth = measureText(selectedBuf); - if (selectedWidth == 0) { - selectedWidth = gDisplay->getFontWidth() == 0 ? 1 : gDisplay->getFontWidth(); + gDisplay->setDrawColor(highlightRow ? 0 : 1); + gDisplay->drawFrame(valueX, boxY, boxSize, boxSize); + if (graphicalItem->graphicalToggleState() && boxSize > 4) { + gDisplay->drawBox(valueX + 2, boxY + 2, boxSize - 4, boxSize - 4); + } + drawnValueWidth = boxSize; + } else { + const char* valuePtr = value; + uint8_t valueLen = safeLength(value); + if (hasFocus) { + if (widgetEditingSelection) { + uint8_t selectionStart = valueSelectionStart > valueLen ? valueLen : valueSelectionStart; + uint16_t rawSelectionEnd = static_cast(valueSelectionStart) + valueSelectionLength; + uint8_t selectionEnd = rawSelectionEnd > valueLen ? valueLen : static_cast(rawSelectionEnd); + + if (selectionEnd <= selectionStart && selectionStart < valueLen) { + selectionEnd = selectionStart + 1; + } + + valueShift = selectionStart; + + while (valueShift > 0) { + char rangeBuf[textBufferSize]; + uint8_t candidateShift = valueShift - 1; + uint8_t rangeLen = static_cast(selectionEnd - candidateShift); + copyTextRange(value, candidateShift, rangeLen, rangeBuf); + if (measureText(rangeBuf) > alignedValueWidth) { + break; + } + valueShift = candidateShift; + } + + while (valueShift < selectionStart) { + char rangeBuf[textBufferSize]; + uint8_t rangeLen = static_cast(selectionEnd - valueShift); + copyTextRange(value, valueShift, rangeLen, rangeBuf); + if (measureText(rangeBuf) <= alignedValueWidth) { + break; + } + valueShift++; + } + } else if (viewShift > labelLen) { + valueShift = viewShift - labelLen - 1; } + } + + valuePtr = valueShift < valueLen ? value + valueShift : ""; + + char valueBuf[textBufferSize]; + copyTextWindowByWidth(valuePtr, alignedValueWidth, gDisplay, valueBuf); + + drawnValueWidth = measureText(valueBuf); + valueX = valueRight > drawnValueWidth ? valueRight - drawnValueWidth : valueLeft; + + gDisplay->setDrawColor(highlightRow ? 0 : 1); + gDisplay->setCursor(valueX, baseline); + gDisplay->draw(valueBuf); - 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; + if (hasFocus && editing && hasValueSelection) { + uint16_t valueLen16 = safeLength(value); + uint16_t selectionStart = valueSelectionStart; + uint16_t selectionEnd = static_cast(valueSelectionStart) + valueSelectionLength; + + if (selectionStart > valueLen16) { + selectionStart = valueLen16; + } + if (selectionEnd > valueLen16) { + selectionEnd = valueLen16; } + if (selectionEnd <= selectionStart && selectionStart < valueLen16) { + selectionEnd = selectionStart + 1; + } + + uint16_t visibleStart = valueShift; + uint8_t visibleChars = safeLength(valueBuf); + uint16_t visibleEnd = static_cast(visibleStart) + visibleChars; + + uint16_t overlapStart = selectionStart > visibleStart ? selectionStart : visibleStart; + uint16_t overlapEnd = selectionEnd < visibleEnd ? selectionEnd : visibleEnd; + + if (overlapEnd > overlapStart) { + uint8_t relativeStart = static_cast(overlapStart - visibleStart); + uint8_t relativeLen = static_cast(overlapEnd - overlapStart); + + char prefixBuf[textBufferSize]; + char segmentBuf[textBufferSize]; + copyTextRange(valueBuf, 0, relativeStart, prefixBuf); + copyTextRange(valueBuf, relativeStart, relativeLen, segmentBuf); + + uint8_t prefixWidth = measureText(prefixBuf); + uint8_t segmentWidth = measureText(segmentBuf); + if (segmentWidth == 0) { + segmentWidth = charW; + } + + uint8_t segmentX = valueX + prefixWidth; + uint8_t selectionPad = tightSelectionBox ? 0 : 1; + uint8_t highlightX = segmentX > selectionPad ? static_cast(segmentX - selectionPad) : 0; + uint16_t highlightRight = static_cast(segmentX) + segmentWidth + selectionPad; + if (highlightRight > valueRight) { + highlightRight = valueRight; + } + uint8_t highlightWidth = highlightRight > highlightX + ? static_cast(highlightRight - highlightX) + : 0; - uint8_t highlightWidth = highlightRight > highlightX - ? static_cast(highlightRight - highlightX) - : 0; - if (highlightWidth > 0) { gDisplay->setDrawColor(1); - gDisplay->drawBox(highlightX, yTop, highlightWidth, h); + if (highlightWidth > 0) { + gDisplay->drawBox(highlightX, top, highlightWidth, rowH); + } + gDisplay->setDrawColor(0); - gDisplay->setCursor(selectedX, baseline); - gDisplay->draw(selectedBuf); + gDisplay->setCursor(segmentX, baseline); + gDisplay->draw(segmentBuf); + gDisplay->setDrawColor(1); } } } - } else { - if (graphicalItem != NULL && graphicalItem->hasGraphicalToggle()) { - uint8_t box = toggleIndicatorWidth(); - uint8_t xBox = contentRight > rightPadding + box ? contentRight - rightPadding - box : x; - uint8_t yBox = yTop + (h > box ? (h - box) / 2 : 0); - gDisplay->drawFrame(xBox, yBox, box, box); - if (graphicalItem->graphicalToggleState() && box > 4) { - gDisplay->drawBox(xBox + 2, yBox + 2, box - 4, box - 4); - } - } + } + + gDisplay->setDrawColor(1); + + if (hasFocus) { + uint8_t cursorX = !hasValue + ? static_cast(textX + drawnLabelWidth) + : static_cast(valueX + drawnValueWidth); + moveCursor(cursorX / charW, cursorRow); } if (cursorRow == 0) { drawScrollBar(); } - - gDisplay->setDrawColor(1); } void GraphicalDisplayRenderer::clearBlinker() {} @@ -317,47 +509,62 @@ void GraphicalDisplayRenderer::drawBlinker() { if (!MenuItem::isEditing()) { return; } + uint8_t h = rowHeight(); uint8_t top = cursorPixelY + 1 > h ? cursorPixelY + 1 - h : 0; - gDisplay->drawBox(cursorPixelX, top, 1, h); + + gDisplay->setDrawColor(1); + gDisplay->drawBox(cursorPixelX, top + 1, 1, h > 2 ? h - 2 : 1); } void GraphicalDisplayRenderer::moveCursor(uint8_t col, uint8_t row) { MenuRenderer::moveCursor(col, row); + uint8_t charW = gDisplay->getFontWidth() == 0 ? 1 : gDisplay->getFontWidth(); - uint8_t h = rowHeight(); + uint8_t rowH = rowHeight(); + cursorPixelX = col * charW; - cursorPixelY = row * h + h - 1; + cursorPixelY = row * rowH + rowH - 1; gDisplay->setCursor(cursorPixelX, cursorPixelY); } void GraphicalDisplayRenderer::drawSubMenuIndicator() { - uint8_t h = rowHeight(); - uint8_t top = cursorRow * h; - uint8_t rightInset = totalItems > getMaxRows() ? scrollbarWidth + scrollbarGap : 0; - uint8_t contentRight = gDisplay->getDisplayWidth() > rightInset ? gDisplay->getDisplayWidth() - rightInset : gDisplay->getDisplayWidth(); + uint8_t rowH = rowHeight(); + uint8_t top = cursorRow * rowH; + + bool showScrollBar = totalItems > getMaxRows(); + uint8_t rightInset = showScrollBar ? scrollbarWidth + scrollbarGap : 0; + uint8_t contentRight = + gDisplay->getDisplayWidth() > rightInset ? gDisplay->getDisplayWidth() - rightInset : gDisplay->getDisplayWidth(); + uint8_t x = contentRight > rightPadding + submenuGlyphWidth ? contentRight - rightPadding - submenuGlyphWidth : leftPadding; - uint8_t y = top + (h > submenuGlyphHeight ? (h - submenuGlyphHeight) / 2 : 0); + uint8_t y = top + (rowH > submenuGlyphHeight ? (rowH - submenuGlyphHeight) / 2 : 0); gDisplay->setDrawColor(hasFocus && !MenuItem::isEditing() ? 0 : 1); + gDisplay->drawBox(x, y, 1, 1); gDisplay->drawBox(x, y + 1, 2, 1); gDisplay->drawBox(x, y + 2, 3, 1); gDisplay->drawBox(x, y + 3, 2, 1); gDisplay->drawBox(x, y + 4, 1, 1); + gDisplay->setDrawColor(1); } void GraphicalDisplayRenderer::drawListIndicator() { - uint8_t h = rowHeight(); - uint8_t top = cursorRow * h; - uint8_t rightInset = totalItems > getMaxRows() ? scrollbarWidth + scrollbarGap : 0; - uint8_t contentRight = gDisplay->getDisplayWidth() > rightInset ? gDisplay->getDisplayWidth() - rightInset : gDisplay->getDisplayWidth(); + uint8_t rowH = rowHeight(); + uint8_t top = cursorRow * rowH; + + bool showScrollBar = totalItems > getMaxRows(); + uint8_t rightInset = showScrollBar ? scrollbarWidth + scrollbarGap : 0; + uint8_t contentRight = + gDisplay->getDisplayWidth() > rightInset ? gDisplay->getDisplayWidth() - rightInset : gDisplay->getDisplayWidth(); + uint8_t x = contentRight > rightPadding + listGlyphWidth ? contentRight - rightPadding - listGlyphWidth : leftPadding; - uint8_t y = top + (h > listGlyphHeight ? (h - listGlyphHeight) / 2 : 0); + uint8_t y = top + (rowH > listGlyphHeight ? (rowH - listGlyphHeight) / 2 : 0); gDisplay->setDrawColor(hasFocus && !MenuItem::isEditing() ? 0 : 1); - gDisplay->drawXbm(x, y, listGlyphWidth, listGlyphHeight, listGlyph); + gDisplay->drawXbm(x, y, listGlyphWidth, listGlyphHeight, updownGlyph); gDisplay->setDrawColor(1); } @@ -369,8 +576,8 @@ uint8_t GraphicalDisplayRenderer::measureText(const char* text) const { } uint8_t GraphicalDisplayRenderer::toggleIndicatorWidth() const { - uint8_t h = rowHeight(); - return h > 4 ? h - 4 : h; + uint8_t width = rowHeight() > 4 ? rowHeight() - 4 : rowHeight(); + return width < 3 ? 3 : width; } uint8_t GraphicalDisplayRenderer::rowHeight() const { @@ -379,7 +586,10 @@ uint8_t GraphicalDisplayRenderer::rowHeight() const { uint8_t GraphicalDisplayRenderer::getMaxRows() const { uint8_t h = rowHeight(); - return h == 0 ? 0 : gDisplay->getDisplayHeight() / h; + if (h == 0) { + return 0; + } + return gDisplay->getDisplayHeight() / h; } uint8_t GraphicalDisplayRenderer::getMaxCols() const { @@ -394,12 +604,11 @@ uint8_t GraphicalDisplayRenderer::getMaxCols() const { } uint8_t GraphicalDisplayRenderer::getEffectiveCols() const { - uint8_t charW = gDisplay->getFontWidth(); - if (charW == 0) { - charW = 1; - } + uint8_t charW = gDisplay->getFontWidth() == 0 ? 1 : gDisplay->getFontWidth(); + + bool showScrollBar = totalItems > getMaxRows(); + uint8_t rightInset = showScrollBar ? scrollbarWidth + scrollbarGap : 0; - uint8_t rightInset = totalItems > getMaxRows() ? scrollbarWidth + scrollbarGap : 0; uint8_t usable = gDisplay->getDisplayWidth(); if (usable <= rightInset + leftPadding) { return 0; @@ -407,13 +616,11 @@ uint8_t GraphicalDisplayRenderer::getEffectiveCols() const { usable -= rightInset + leftPadding; uint8_t cols = usable / charW; - uint8_t iconWidth = measureText(cursorIcon); - uint8_t editIconWidth = measureText(editCursorIcon); - if (editIconWidth > iconWidth) { - iconWidth = editIconWidth; + uint8_t editWidth = measureText(editCursorIcon); + if (editWidth > iconWidth) { + iconWidth = editWidth; } - uint8_t iconCols = static_cast((iconWidth + charW - 1) / charW); if (cols > iconCols) { cols -= iconCols; @@ -451,5 +658,6 @@ void GraphicalDisplayRenderer::drawScrollBar() { uint16_t trackRange = areaHeight > handleHeight ? areaHeight - handleHeight : 0; uint16_t scrollRange = totalItems > rows ? totalItems - rows : 1; uint8_t y = (static_cast(viewStart) * trackRange) / scrollRange; + gDisplay->drawBox(x, y, scrollbarWidth, handleHeight); } diff --git a/src/renderer/GraphicalDisplayRenderer.h b/src/renderer/GraphicalDisplayRenderer.h index f3c16d2e..ab4bc321 100644 --- a/src/renderer/GraphicalDisplayRenderer.h +++ b/src/renderer/GraphicalDisplayRenderer.h @@ -19,8 +19,6 @@ class GraphicalDisplayRenderer : public MenuRenderer, public GraphicalValueSelectionRenderer, public GraphicalRendererContext { private: - friend class FocusableGraphicalDisplayRenderer; - GraphicalDisplayInterface* gDisplay; const uint8_t* defaultFont = NULL; @@ -33,6 +31,7 @@ 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; @@ -81,6 +80,7 @@ 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; diff --git a/test/GraphicalDisplayRenderer.cpp b/test/GraphicalDisplayRenderer.cpp index d71e8eb3..8ad439b2 100644 --- a/test/GraphicalDisplayRenderer.cpp +++ b/test/GraphicalDisplayRenderer.cpp @@ -3,29 +3,54 @@ #include #include +#include + class StubGraphicalDisplay : public GraphicalDisplayInterface { public: + uint8_t displayWidth = 128; + uint8_t displayHeight = 64; + uint8_t fontWidth = 6; + uint8_t fontHeight = 8; uint8_t drawBoxCount = 0; uint8_t drawXbmCount = 0; + uint8_t drawFrameCount = 0; uint8_t drawColorCount = 0; - uint8_t drawColors[4] = {0, 0, 0, 0}; + uint8_t drawColors[8] = {0, 0, 0, 0, 0, 0, 0, 0}; uint8_t lastDrawBoxHeight = 0; uint8_t lastDrawBoxY = 0; + uint8_t lastDrawFrameWidth = 0; + uint8_t lastCursorX = 0; + uint8_t lastCursorY = 0; + uint8_t drawTextCount = 0; + char drawTextLog[4][16] = {{0}}; 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 draw(const char* text) override { + if (drawTextCount < 4) { + uint8_t i = 0; + while (text != NULL && text[i] != '\0' && i < 15) { + drawTextLog[drawTextCount][i] = text[i]; + i++; + } + drawTextLog[drawTextCount][i] = '\0'; + } + drawTextCount++; + } + void setCursor(uint8_t x, uint8_t y) override { + lastCursorX = x; + lastCursorY = y; + } 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 getDisplayWidth() const override { return displayWidth; } + uint8_t getDisplayHeight() const override { return displayHeight; } + uint8_t getFontWidth() const override { return fontWidth; } + uint8_t getFontHeight() const override { return fontHeight; } uint8_t getTextWidth(const char* text) override { if (text == NULL) { return 0; @@ -34,10 +59,10 @@ class StubGraphicalDisplay : public GraphicalDisplayInterface { while (text[len] != '\0') { len++; } - return static_cast(len * 6); + return static_cast(len * fontWidth); } void setDrawColor(uint8_t color) override { - if (drawColorCount < 4) { + if (drawColorCount < 8) { drawColors[drawColorCount] = color; } drawColorCount++; @@ -49,7 +74,10 @@ class StubGraphicalDisplay : public GraphicalDisplayInterface { lastDrawBoxY = y; lastDrawBoxHeight = h; } - void drawFrame(uint8_t, uint8_t, uint8_t, uint8_t) override {} + void drawFrame(uint8_t, uint8_t, uint8_t w, uint8_t) override { + drawFrameCount++; + lastDrawFrameWidth = w; + } void drawXbm(uint8_t, uint8_t, uint8_t, uint8_t, const uint8_t*) override { drawXbmCount++; } @@ -57,8 +85,6 @@ class StubGraphicalDisplay : public GraphicalDisplayInterface { class FocusableGraphicalDisplayRenderer : public GraphicalDisplayRenderer { public: - using GraphicalDisplayRenderer::getEffectiveCols; - using GraphicalDisplayRenderer::getMaxCols; using GraphicalDisplayRenderer::GraphicalDisplayRenderer; void setFocusForTest(bool focused) { @@ -66,26 +92,22 @@ class FocusableGraphicalDisplayRenderer : public GraphicalDisplayRenderer { } }; -unittest(graphical_renderer_reports_effective_cols_with_cursor_icons) { - StubGraphicalDisplay display; - - FocusableGraphicalDisplayRenderer plain(&display); - FocusableGraphicalDisplayRenderer iconed(&display, NULL, "[]", "[e]"); - - assertEqual(21, plain.getEffectiveCols()); - assertEqual(18, iconed.getEffectiveCols()); - - iconed.setViewportContext(0, 9); - assertEqual(17, iconed.getEffectiveCols()); -} +class ToggleGraphicalItem : public BasicItem { + public: + explicit ToggleGraphicalItem(const char* text) : BasicItem(text) {} + bool hasGraphicalToggle() const override { return true; } + bool graphicalToggleState() const override { return true; } +}; -unittest(graphical_renderer_reports_max_cols_from_font_width) { +unittest(graphical_renderer_draws_placeholder_cursor_for_unfocused_rows) { StubGraphicalDisplay display; - FocusableGraphicalDisplayRenderer renderer(&display); + GraphicalDisplayRenderer renderer(&display, NULL, ">>", "[]"); - renderer.begin(); + renderer.drawItem("Label", NULL); - assertEqual(21, renderer.getMaxCols()); + assertTrue(display.drawTextCount >= 2); + assertTrue(strcmp(display.drawTextLog[0], " ") == 0); + assertTrue(strcmp(display.drawTextLog[1], "Label") == 0); } unittest(graphical_renderer_exposes_value_selection_extension) { @@ -111,6 +133,33 @@ unittest(graphical_renderer_uses_tight_row_height) { assertEqual(8, display.lastDrawBoxHeight); } +unittest(graphical_renderer_toggle_keeps_minimum_box_width) { + StubGraphicalDisplay display; + display.fontHeight = 4; + + FocusableGraphicalDisplayRenderer renderer(&display); + ToggleGraphicalItem toggleItem("Toggle"); + + renderer.setFocusForTest(true); + renderer.setActiveItem(&toggleItem); + renderer.drawItem("Toggle", NULL); + + assertEqual(1, display.drawFrameCount); + assertEqual(3, display.lastDrawFrameWidth); +} + +unittest(graphical_renderer_does_not_reserve_third_for_short_value) { + StubGraphicalDisplay display; + display.displayWidth = 48; + GraphicalDisplayRenderer renderer(&display); + + renderer.drawItem("ABCDEFGHIJ", "1"); + + assertTrue(display.drawTextCount >= 2); + assertTrue(strcmp(display.drawTextLog[0], "ABCDEF") == 0); + assertTrue(strcmp(display.drawTextLog[1], "1") == 0); +} + unittest(graphical_renderer_clears_scrollbar_track_before_handle) { StubGraphicalDisplay display; GraphicalDisplayRenderer renderer(&display); From fa80938d7db4dfa1d2d24ffc35fca7b5b0675698 Mon Sep 17 00:00:00 2001 From: forntoh Date: Tue, 26 May 2026 19:30:04 +0200 Subject: [PATCH 2/2] test: align graphical renderer assertions --- test/GraphicalDisplayRenderer.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/GraphicalDisplayRenderer.cpp b/test/GraphicalDisplayRenderer.cpp index 8ad439b2..01857302 100644 --- a/test/GraphicalDisplayRenderer.cpp +++ b/test/GraphicalDisplayRenderer.cpp @@ -145,7 +145,7 @@ unittest(graphical_renderer_toggle_keeps_minimum_box_width) { renderer.drawItem("Toggle", NULL); assertEqual(1, display.drawFrameCount); - assertEqual(3, display.lastDrawFrameWidth); + assertEqual(4, display.lastDrawFrameWidth); } unittest(graphical_renderer_does_not_reserve_third_for_short_value) { @@ -170,8 +170,9 @@ unittest(graphical_renderer_clears_scrollbar_track_before_handle) { assertEqual(3, display.drawBoxCount); assertEqual(0, display.drawColors[0]); assertEqual(1, display.drawColors[1]); - assertEqual(0, display.drawColors[2]); - assertEqual(1, display.drawColors[3]); + assertTrue(display.drawColorCount >= 4); + assertEqual(0, display.drawColors[display.drawColorCount - 2]); + assertEqual(1, display.drawColors[display.drawColorCount - 1]); assertEqual(0, display.lastDrawBoxY); }