From e6dbb99896a838a97c31789537bd8083a886e29c Mon Sep 17 00:00:00 2001 From: forntoh Date: Fri, 24 Apr 2026 00:16:00 +0200 Subject: [PATCH 1/2] refactor: add graphical viewport context to MenuScreen --- .../overview/rendering/graphical-display.rst | 7 + src/MenuScreen.cpp | 302 ++++++++++++++++-- test/MenuScreenTest.cpp | 4 +- test/UnselectableItem.cpp | 2 +- 4 files changed, 292 insertions(+), 23 deletions(-) diff --git a/docs/source/overview/rendering/graphical-display.rst b/docs/source/overview/rendering/graphical-display.rst index 55c085fc..27c28170 100644 --- a/docs/source/overview/rendering/graphical-display.rst +++ b/docs/source/overview/rendering/graphical-display.rst @@ -17,6 +17,13 @@ Features - Buffered drawing with renderer-managed frame flushing - Per-item custom fonts (any U8g2 font) +Menu integration +---------------- + +``MenuScreen`` provides viewport and active-item context to graphical renderers +through ``GraphicalRendererContext``. This lets the renderer recompute visible +rows and value-column widths when fonts differ per item. + Basic usage ----------- diff --git a/src/MenuScreen.cpp b/src/MenuScreen.cpp index c5fd05bf..704dcb3e 100644 --- a/src/MenuScreen.cpp +++ b/src/MenuScreen.cpp @@ -1,13 +1,62 @@ #include "MenuScreen.h" +#include "display/GraphicalDisplayInterface.h" #include "renderer/FrameLifecycleRenderer.h" +#include "renderer/GraphicalMenuItem.h" +#include "renderer/GraphicalRendererContext.h" namespace { +uint8_t getVisibleGraphicalValueWidth( + const std::vector& items, + uint8_t view, + uint8_t rows, + GraphicalDisplayInterface* display, + GraphicalRendererContext* context) { + if (display == NULL || context == NULL) { + return 0; + } + + uint8_t widest = 0; + for (uint8_t i = 0; i < rows && (view + i) < items.size(); i++) { + MenuItem* item = items[view + i]; + if (item == NULL) { + continue; + } + + context->setActiveItem(item); + const GraphicalMenuItem* graphicalItem = + static_cast(item->queryCapability(GraphicalMenuItem::capabilityId())); + uint8_t width = graphicalItem == NULL ? 0 : graphicalItem->measureGraphicalValueWidth(display); + if (width > widest) { + widest = width; + } + } + + context->setActiveItem(NULL); + return widest; +} + +GraphicalRendererContext* toGraphicalContext(MenuRenderer* renderer) { + if (renderer == NULL) { + return NULL; + } + return static_cast( + renderer->queryExtension(GraphicalRendererContext::extensionId())); +} + FrameLifecycleRenderer* toFrameLifecycle(MenuRenderer* renderer) { if (renderer == NULL) { return NULL; } return static_cast(renderer->queryExtension(FrameLifecycleRenderer::extensionId())); } + +GraphicalDisplayInterface* toGraphicalDisplay(MenuRenderer* renderer) { + GraphicalRendererContext* context = toGraphicalContext(renderer); + if (context == NULL) { + return NULL; + } + return context->getGraphicalDisplay(); +} } // namespace void MenuScreen::setParent(MenuScreen* parent) { @@ -29,9 +78,11 @@ MenuItem* MenuScreen::operator[](const uint8_t position) { void MenuScreen::setCursor(MenuRenderer* renderer, uint8_t position) { if (items.empty()) { cursor = 0; + view = 0; draw(renderer); return; } + uint8_t constrained = constrain(position, 0, items.size() - 1); if (!items[constrained]->isSelectable()) { uint8_t forward = constrained; @@ -48,50 +99,161 @@ void MenuScreen::setCursor(MenuRenderer* renderer, uint8_t position) { constrained = backward < 0 ? constrained : static_cast(backward); } } - if (constrained == cursor) { - return; + + uint8_t previousView = view; + uint8_t viewSize = renderer->getMaxRows(); + if (viewSize == 0) { + viewSize = 1; } - uint8_t viewSize = renderer->maxRows; if (constrained < view) { view = constrained; } else if (constrained > (view + (viewSize - 1))) { view = constrained - (viewSize - 1); } + + if (constrained == cursor && previousView == view) { + return; + } + cursor = constrained; draw(renderer); } void MenuScreen::draw(MenuRenderer* renderer) { + GraphicalRendererContext* graphicalContext = toGraphicalContext(renderer); FrameLifecycleRenderer* frameLifecycle = toFrameLifecycle(renderer); + GraphicalDisplayInterface* graphicalDisplay = toGraphicalDisplay(renderer); + + uint8_t rows = renderer->getMaxRows(); + if (rows == 0) { + return; + } + + if (items.empty()) { + cursor = 0; + view = 0; + + if (graphicalContext != NULL) { + graphicalContext->setViewportContext(0, 0); + graphicalContext->setValueAreaWidth(0); + graphicalContext->setActiveItem(NULL); + } + + if (frameLifecycle != NULL) { + frameLifecycle->beginFrame(); + frameLifecycle->endFrame(); + } + return; + } + + if (cursor >= items.size()) { + cursor = items.size() - 1; + } + + uint8_t maxView = items.size() > rows ? items.size() - rows : 0; + if (view > maxView) { + view = maxView; + } + + if (cursor < view) { + cursor = view; + } else if (cursor >= view + rows) { + cursor = view + rows - 1; + } + + if (graphicalContext != NULL) { + graphicalContext->setViewportContext(view, items.size()); + + uint8_t valueWidth = getVisibleGraphicalValueWidth(items, view, rows, graphicalDisplay, graphicalContext); + uint8_t recalculatedRows = renderer->getMaxRows(); + if (recalculatedRows == 0) { + recalculatedRows = 1; + } + + if (recalculatedRows != rows) { + rows = recalculatedRows; + maxView = items.size() > rows ? items.size() - rows : 0; + if (view > maxView) { + view = maxView; + } + if (cursor < view) { + cursor = view; + } else if (cursor >= view + rows) { + cursor = view + rows - 1; + } + + graphicalContext->setViewportContext(view, items.size()); + valueWidth = getVisibleGraphicalValueWidth(items, view, rows, graphicalDisplay, graphicalContext); + } + + graphicalContext->setValueAreaWidth(valueWidth); + } + if (frameLifecycle != NULL) { frameLifecycle->beginFrame(); } - for (uint8_t i = 0; i < renderer->maxRows && i < items.size(); i++) { + for (uint8_t i = 0; i < rows && (view + i) < items.size(); i++) { MenuItem* item = this->items[view + i]; - if (item == nullptr) { - break; + if (item == NULL) { + continue; } + syncIndicators(i, renderer); + + if (graphicalContext != NULL) { + graphicalContext->setActiveItem(item); + } + item->draw(renderer); } + if (graphicalContext != NULL) { + graphicalContext->setActiveItem(NULL); + } + if (frameLifecycle != NULL) { frameLifecycle->endFrame(); } } void MenuScreen::syncIndicators(uint8_t index, MenuRenderer* renderer) { + uint8_t rows = renderer->getMaxRows(); renderer->hasHiddenItemsAbove = index == 0 && view > 0; - renderer->hasHiddenItemsBelow = index == renderer->maxRows - 1 && (view + renderer->maxRows) < items.size(); + renderer->hasHiddenItemsBelow = + rows > 0 && index == rows - 1 && (view + rows) < items.size(); renderer->hasFocus = cursor == view + index; renderer->cursorRow = index; } bool MenuScreen::process(LcdMenu* menu, const unsigned char command) { MenuRenderer* renderer = menu->getRenderer(); - syncIndicators(cursor - view, renderer); - if (items[cursor]->process(menu, command)) return true; + GraphicalRendererContext* graphicalContext = toGraphicalContext(renderer); + + if (graphicalContext != NULL) { + graphicalContext->setActiveItem(NULL); + graphicalContext->setViewportContext(view, items.size()); + } + + if (!items.empty()) { + uint8_t focusIndex = cursor >= view ? cursor - view : 0; + syncIndicators(focusIndex, renderer); + if (graphicalContext != NULL) { + graphicalContext->setActiveItem(items[cursor]); + } + + if (items[cursor]->process(menu, command)) { + if (graphicalContext != NULL) { + graphicalContext->setActiveItem(NULL); + } + return true; + } + + if (graphicalContext != NULL) { + graphicalContext->setActiveItem(NULL); + } + } + switch (command) { case UP: renderer->viewShift = 0; @@ -104,17 +266,22 @@ bool MenuScreen::process(LcdMenu* menu, const unsigned char command) { case BACK: renderer->viewShift = 0; if (parent != NULL) { + uint8_t parentCursor = parent->getCursor(); menu->setScreen(parent); + menu->setCursor(parentCursor); } LOG(F("MenuScreen::back")); return true; case RIGHT: - if (renderer->cursorCol >= renderer->maxCols - 1) { - renderer->viewShift++; - draw(renderer); + { + uint8_t maxCols = renderer->getMaxCols(); + if (maxCols > 0 && renderer->cursorCol >= maxCols - 1) { + renderer->viewShift++; + draw(renderer); + } + LOG(F("MenuScreen::right"), renderer->viewShift); + return true; } - LOG(F("MenuScreen::right"), renderer->viewShift); - return true; case LEFT: if (renderer->viewShift > 0) { renderer->viewShift--; @@ -130,11 +297,23 @@ bool MenuScreen::process(LcdMenu* menu, const unsigned char command) { void MenuScreen::up(MenuRenderer* renderer) { if (items.empty()) { cursor = 0; + view = 0; draw(renderer); return; } + if (cursor > 0) { - setCursor(renderer, cursor - 1); + int16_t target = static_cast(cursor) - 1; + while (target >= 0 && !items[static_cast(target)]->isSelectable()) { + target--; + } + + if (target >= 0) { + setCursor(renderer, static_cast(target)); + } else if (view > 0) { + view--; + draw(renderer); + } } else if (view > 0) { view--; draw(renderer); @@ -145,12 +324,24 @@ void MenuScreen::up(MenuRenderer* renderer) { void MenuScreen::down(MenuRenderer* renderer) { if (items.empty()) { cursor = 0; + view = 0; draw(renderer); return; } + if (cursor < items.size() - 1) { - setCursor(renderer, cursor + 1); - } else if (view + renderer->maxRows < items.size()) { + uint16_t target = static_cast(cursor) + 1; + while (target < items.size() && !items[static_cast(target)]->isSelectable()) { + target++; + } + + if (target < items.size()) { + setCursor(renderer, static_cast(target)); + } else if (view + renderer->getMaxRows() < items.size()) { + view++; + draw(renderer); + } + } else if (view + renderer->getMaxRows() < items.size()) { view++; draw(renderer); } @@ -196,20 +387,91 @@ void MenuScreen::clear() { } bool MenuScreen::poll(MenuRenderer* renderer, uint16_t pollInterval) { + GraphicalRendererContext* graphicalContext = toGraphicalContext(renderer); + GraphicalDisplayInterface* graphicalDisplay = toGraphicalDisplay(renderer); + static unsigned long lastPollTime = 0; if (millis() - lastPollTime < pollInterval) { return false; } + lastPollTime = millis(); + + if (graphicalContext != NULL) { + graphicalContext->setActiveItem(NULL); + graphicalContext->setViewportContext(view, items.size()); + } + + if (items.empty() || MenuItem::isEditing()) { + return false; + } + + uint8_t rows = renderer->getMaxRows(); + if (rows == 0) { + return false; + } + + if (cursor >= items.size()) { + cursor = items.size() - 1; + } + + uint8_t maxView = items.size() > rows ? items.size() - rows : 0; + if (view > maxView) { + view = maxView; + } + + if (cursor < view) { + cursor = view; + } else if (cursor >= view + rows) { + cursor = view + rows - 1; + } + + if (graphicalContext != NULL) { + uint8_t valueWidth = getVisibleGraphicalValueWidth(items, view, rows, graphicalDisplay, graphicalContext); + uint8_t recalculatedRows = renderer->getMaxRows(); + if (recalculatedRows == 0) { + recalculatedRows = 1; + } + + if (recalculatedRows != rows) { + rows = recalculatedRows; + maxView = items.size() > rows ? items.size() - rows : 0; + if (view > maxView) { + view = maxView; + } + if (cursor < view) { + cursor = view; + } else if (cursor >= view + rows) { + cursor = view + rows - 1; + } + + graphicalContext->setViewportContext(view, items.size()); + valueWidth = getVisibleGraphicalValueWidth(items, view, rows, graphicalDisplay, graphicalContext); + } + + graphicalContext->setValueAreaWidth(valueWidth); + } + bool redrawn = false; - for (uint8_t i = 0; i < renderer->maxRows && (view + i) < items.size(); i++) { + for (uint8_t i = 0; i < rows && (view + i) < items.size(); i++) { MenuItem* item = this->items[view + i]; - if (item == nullptr || !item->polling || MenuItem::isEditing()) continue; + if (item == NULL || !item->polling) { + continue; + } + syncIndicators(i, renderer); + + if (graphicalContext != NULL) { + graphicalContext->setActiveItem(item); + } + item->draw(renderer); redrawn = true; } - lastPollTime = millis(); + if (graphicalContext != NULL) { + graphicalContext->setActiveItem(NULL); + } + return redrawn; } diff --git a/test/MenuScreenTest.cpp b/test/MenuScreenTest.cpp index 70feb62b..5a4432b2 100644 --- a/test/MenuScreenTest.cpp +++ b/test/MenuScreenTest.cpp @@ -102,8 +102,8 @@ unittest(menu_screen_draw_clamps_view) { screen.draw(&renderer); assertEqual((uint8_t)0, i1->drawCount); - assertEqual((uint8_t)0, i2->drawCount); - assertEqual((uint8_t)0, i3->drawCount); // no items drawn but no crash + assertEqual((uint8_t)1, i2->drawCount); + assertEqual((uint8_t)1, i3->drawCount); delete i1; delete i2; diff --git a/test/UnselectableItem.cpp b/test/UnselectableItem.cpp index 9818f625..2f5dde96 100644 --- a/test/UnselectableItem.cpp +++ b/test/UnselectableItem.cpp @@ -79,7 +79,7 @@ unittest(up_shifts_view_when_label_offscreen) { screen.setCursor(&renderer, 1); // keep view=1 screen.up(&renderer); assertEqual((uint8_t)1, screen.getCursor()); - assertEqual((uint8_t)1, screen.view); + assertEqual((uint8_t)0, screen.view); delete label; delete first; delete second; From cea88e3aaa8e88ae12823659a09ef526958695f2 Mon Sep 17 00:00:00 2001 From: forntoh Date: Fri, 24 Apr 2026 09:42:01 +0200 Subject: [PATCH 2/2] refactor: extract MenuScreen viewport preparation helper --- src/MenuScreen.cpp | 135 +++++++++++++++++++-------------------------- 1 file changed, 56 insertions(+), 79 deletions(-) diff --git a/src/MenuScreen.cpp b/src/MenuScreen.cpp index 704dcb3e..21fc9be8 100644 --- a/src/MenuScreen.cpp +++ b/src/MenuScreen.cpp @@ -57,6 +57,56 @@ GraphicalDisplayInterface* toGraphicalDisplay(MenuRenderer* renderer) { } return context->getGraphicalDisplay(); } + +void clampViewport(uint8_t& cursor, uint8_t& view, size_t itemCount, uint8_t rows) { + if (itemCount == 0 || rows == 0) { + return; + } + + if (cursor >= itemCount) { + cursor = itemCount - 1; + } + + uint8_t maxView = itemCount > rows ? itemCount - rows : 0; + if (view > maxView) { + view = maxView; + } + + if (cursor < view) { + cursor = view; + } else if (cursor >= view + rows) { + cursor = view + rows - 1; + } +} + +uint8_t prepareViewport( + const std::vector& items, + uint8_t& cursor, + uint8_t& view, + MenuRenderer* renderer, + GraphicalRendererContext* graphicalContext, + GraphicalDisplayInterface* graphicalDisplay, + uint8_t rows) { + clampViewport(cursor, view, items.size(), rows); + + graphicalContext->setViewportContext(view, items.size()); + + uint8_t valueWidth = getVisibleGraphicalValueWidth(items, view, rows, graphicalDisplay, graphicalContext); + uint8_t recalculatedRows = renderer->getMaxRows(); + if (recalculatedRows == 0) { + recalculatedRows = 1; + } + + if (recalculatedRows != rows) { + rows = recalculatedRows; + clampViewport(cursor, view, items.size(), rows); + graphicalContext->setViewportContext(view, items.size()); + valueWidth = getVisibleGraphicalValueWidth(items, view, rows, graphicalDisplay, graphicalContext); + } + + graphicalContext->setValueAreaWidth(valueWidth); + return rows; +} } // namespace void MenuScreen::setParent(MenuScreen* parent) { @@ -146,47 +196,10 @@ void MenuScreen::draw(MenuRenderer* renderer) { return; } - if (cursor >= items.size()) { - cursor = items.size() - 1; - } - - uint8_t maxView = items.size() > rows ? items.size() - rows : 0; - if (view > maxView) { - view = maxView; - } - - if (cursor < view) { - cursor = view; - } else if (cursor >= view + rows) { - cursor = view + rows - 1; - } - if (graphicalContext != NULL) { - graphicalContext->setViewportContext(view, items.size()); - - uint8_t valueWidth = getVisibleGraphicalValueWidth(items, view, rows, graphicalDisplay, graphicalContext); - uint8_t recalculatedRows = renderer->getMaxRows(); - if (recalculatedRows == 0) { - recalculatedRows = 1; - } - - if (recalculatedRows != rows) { - rows = recalculatedRows; - maxView = items.size() > rows ? items.size() - rows : 0; - if (view > maxView) { - view = maxView; - } - if (cursor < view) { - cursor = view; - } else if (cursor >= view + rows) { - cursor = view + rows - 1; - } - - graphicalContext->setViewportContext(view, items.size()); - valueWidth = getVisibleGraphicalValueWidth(items, view, rows, graphicalDisplay, graphicalContext); - } - - graphicalContext->setValueAreaWidth(valueWidth); + rows = prepareViewport(items, cursor, view, renderer, graphicalContext, graphicalDisplay, rows); + } else { + clampViewport(cursor, view, items.size(), rows); } if (frameLifecycle != NULL) { @@ -232,7 +245,6 @@ bool MenuScreen::process(LcdMenu* menu, const unsigned char command) { if (graphicalContext != NULL) { graphicalContext->setActiveItem(NULL); - graphicalContext->setViewportContext(view, items.size()); } if (!items.empty()) { @@ -411,45 +423,10 @@ bool MenuScreen::poll(MenuRenderer* renderer, uint16_t pollInterval) { return false; } - if (cursor >= items.size()) { - cursor = items.size() - 1; - } - - uint8_t maxView = items.size() > rows ? items.size() - rows : 0; - if (view > maxView) { - view = maxView; - } - - if (cursor < view) { - cursor = view; - } else if (cursor >= view + rows) { - cursor = view + rows - 1; - } - if (graphicalContext != NULL) { - uint8_t valueWidth = getVisibleGraphicalValueWidth(items, view, rows, graphicalDisplay, graphicalContext); - uint8_t recalculatedRows = renderer->getMaxRows(); - if (recalculatedRows == 0) { - recalculatedRows = 1; - } - - if (recalculatedRows != rows) { - rows = recalculatedRows; - maxView = items.size() > rows ? items.size() - rows : 0; - if (view > maxView) { - view = maxView; - } - if (cursor < view) { - cursor = view; - } else if (cursor >= view + rows) { - cursor = view + rows - 1; - } - - graphicalContext->setViewportContext(view, items.size()); - valueWidth = getVisibleGraphicalValueWidth(items, view, rows, graphicalDisplay, graphicalContext); - } - - graphicalContext->setValueAreaWidth(valueWidth); + rows = prepareViewport(items, cursor, view, renderer, graphicalContext, graphicalDisplay, rows); + } else { + clampViewport(cursor, view, items.size(), rows); } bool redrawn = false;