From 2b064452d53785c60662c0c8467e47987cbf094e Mon Sep 17 00:00:00 2001 From: forntoh Date: Thu, 23 Apr 2026 23:13:52 +0200 Subject: [PATCH 1/5] feat: add graphical renderer foundation with U8g2 adapter --- README.md | 2 +- .../overview/rendering/graphical-display.rst | 40 ++ docs/source/overview/rendering/index.rst | 1 + examples/SSD1306_I2C/SSD1306_I2C.ino | 396 +++++++++++ examples/ST7920_SPI/ST7920_SPI.ino | 32 + platformio.ini | 4 +- src/MenuScreen.cpp | 302 +++++++- src/display/GraphicalDisplayInterface.h | 23 + src/display/U8g2DisplayAdapter.h | 82 +++ src/renderer/GraphicalDisplayRenderer.cpp | 663 ++++++++++++++++++ src/renderer/GraphicalDisplayRenderer.h | 97 +++ src/renderer/GraphicalIndicatorRenderer.h | 16 + src/renderer/GraphicalItemFont.h | 40 ++ src/renderer/GraphicalMenuItem.h | 11 + .../GraphicalValueSelectionRenderer.h | 16 + 15 files changed, 1703 insertions(+), 22 deletions(-) create mode 100644 docs/source/overview/rendering/graphical-display.rst create mode 100644 examples/SSD1306_I2C/SSD1306_I2C.ino create mode 100644 examples/ST7920_SPI/ST7920_SPI.ino create mode 100644 src/display/GraphicalDisplayInterface.h create mode 100644 src/display/U8g2DisplayAdapter.h create mode 100644 src/renderer/GraphicalDisplayRenderer.cpp create mode 100644 src/renderer/GraphicalDisplayRenderer.h create mode 100644 src/renderer/GraphicalIndicatorRenderer.h create mode 100644 src/renderer/GraphicalItemFont.h create mode 100644 src/renderer/GraphicalValueSelectionRenderer.h diff --git a/README.md b/README.md index b2e27f14..b1617def 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ LcdMenu is an open-source Arduino library for creating menu systems. It is designed to be easy to use and flexible enough to support a wide range of use cases. -With LcdMenu, you can create a menu system for your Arduino project with minimal effort. The library provides a simple API for creating menus and handling user input. There are also a number of built-in [display interfaces](reference/api/display/index) to choose from, including LCD displays and OLED displays _(coming soon)_. +With LcdMenu, you can create a menu system for your Arduino project with minimal effort. The library provides a simple API for creating menus and handling user input. There are also a number of built-in [display interfaces](reference/api/display/index) to choose from, including classic character LCDs and graphical displays powered by U8g2 (for example SSD1306 and ST7920 modules).

Example of a menu system created with LcdMenu diff --git a/docs/source/overview/rendering/graphical-display.rst b/docs/source/overview/rendering/graphical-display.rst new file mode 100644 index 00000000..55c085fc --- /dev/null +++ b/docs/source/overview/rendering/graphical-display.rst @@ -0,0 +1,40 @@ +Graphical display renderer +========================== + +The graphical display renderer targets pixel-addressable displays through +:cpp:class:`GraphicalDisplayInterface`. + +It is designed for U8g2-driven modules, including OLED displays such as +SSD1306 and monochrome graphical LCDs such as ST7920. + +Features +-------- + +- Dynamic row and column calculation from the active font metrics +- Row highlighting for selection and value-area highlighting while editing +- Checkbox rendering for boolean items and toggles +- Scrollbar and value indicators for widget-driven items +- Buffered drawing with renderer-managed frame flushing +- Per-item custom fonts (any U8g2 font) + +Basic usage +----------- + +.. code-block:: cpp + + #include + #include + #include + + U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); + U8g2DisplayAdapter display(&u8g2); + GraphicalDisplayRenderer renderer(&display, u8g2_font_6x10_tf); + + MENU_SCREEN(mainScreen, mainItems, + ITEM_FONT(ITEM_LABEL("Overview"), u8g2_font_7x13B_tf), + ITEM_BOOL("Enabled", true, "ON", "OFF", nullptr)); + + void setup() { + renderer.begin(); + renderer.setItemFont(mainItems[1], u8g2_font_6x13_tf); + } diff --git a/docs/source/overview/rendering/index.rst b/docs/source/overview/rendering/index.rst index 7c4cbd19..2767ba27 100644 --- a/docs/source/overview/rendering/index.rst +++ b/docs/source/overview/rendering/index.rst @@ -16,6 +16,7 @@ For example, you could create a renderer for a TFT display, a touchscreen, or ev :caption: The library comes with the following built-in renderers: character-display + graphical-display Don't see a renderer for your favorite output device? Feel free to create a new one and share it with the community! diff --git a/examples/SSD1306_I2C/SSD1306_I2C.ino b/examples/SSD1306_I2C/SSD1306_I2C.ino new file mode 100644 index 00000000..7b44d22b --- /dev/null +++ b/examples/SSD1306_I2C/SSD1306_I2C.ino @@ -0,0 +1,396 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +extern MenuScreen* navigationScreen; +extern MenuScreen* nestedScreen; +extern MenuScreen* inputScreen; +extern MenuScreen* valuesScreen; +extern MenuScreen* rangeListScreen; +extern MenuScreen* widgetScreen; +extern MenuScreen* dynamicScreen; + +char* cloneText(const char* text) { + size_t len = strlen(text); + char* out = new char[len + 1]; + memcpy(out, text, len + 1); + return out; +} + +void refreshMainScreen(); +void jumpToValuesScreen(); +void onUserInput(char* value); +void onNoteInput(char* value); +void onTagInput(char* value); +void clearInputValues(); +void onServiceToggle(bool enabled); +void onServiceToggleBox(bool enabled); +void onRelayToggle(const Ref enabled); +void onAlarmToggle(const bool enabled); +void onAlarmToggleBox(const bool enabled); +void flipRelayExternally(); +void onBrightnessChanged(const Ref value); +void onVolumeChanged(const Ref value); +void onThemeChanged(const uint8_t index); +void onProfileChanged(const Ref index); +void nextProfileExternally(); +void onScheduleWidget(int hour, int minute); +void onControlWidget(uint8_t modeIndex, bool enabled); +void onPidWidget(int kp, int ki, int kd); +void addDynamicItem(); +void insertDynamicItem(); +void removeDynamicAtHead(); +void removeDynamicLast(); +void clearDynamicItems(); +void updateTelemetry(); + +char* userName = cloneText("ALICE"); +char* note = cloneText("HELLO"); +char* tag = cloneText("A1"); +const char* inputCharset = " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; + +unsigned long uptimeSeconds = 0; +float temperatureC = 22.0f; +float voltage = 3.30f; +uint16_t rpm = 1200; + +bool relayEnabled = false; +int brightness = 40; +int volume = 12; +uint8_t profileIndex = 1; + +std::vector themes = {"Classic", "Comfort", "Minimal", "Dense"}; +std::vector profiles = {"A", "B", "C", "D", "E"}; +std::vector modes = {"Auto", "Manual", "Sleep", "Off"}; + +uint8_t dynamicBaseCount = 0; +uint8_t dynamicCounter = 0; + +// clang-format off +MENU_SCREEN(mainScreen, mainItems, + ITEM_FONT(ITEM_LABEL("SSD1306 showcase"), u8g2_font_7x13B_tf), + ITEM_SUBMENU("Navigation", navigationScreen), + ITEM_SUBMENU("Input", inputScreen), + ITEM_SUBMENU("Values / toggles", valuesScreen), + ITEM_SUBMENU("Range / list", rangeListScreen), + ITEM_SUBMENU("Widget combos", widgetScreen), + ITEM_SUBMENU("Dynamic menu", dynamicScreen), + ITEM_COMMAND("Refresh now", refreshMainScreen), + ITEM_BASIC("This row is intentionally very long to test clipping and horizontal shifting")); + +MENU_SCREEN(navigationScreen, navigationItems, + ITEM_BACK(".. Back"), + ITEM_LABEL("Submenu indicator"), + ITEM_SUBMENU("Nested level", nestedScreen), + ITEM_COMMAND("Jump to values", jumpToValuesScreen), + ITEM_BASIC("Use BACK key or .. item to return")); + +MENU_SCREEN(nestedScreen, nestedItems, + ITEM_BACK(".. Back"), + ITEM_BASIC("Nested screen"), + ITEM_BASIC("Submenu arrow should be visible")); + +MENU_SCREEN(inputScreen, inputItems, + ITEM_BACK(".. Back"), + ITEM_LABEL("Input + charset"), + ITEM_INPUT("User", userName, onUserInput), + ITEM_INPUT("Note", note, onNoteInput), + ITEM_INPUT_CHARSET("Tag", tag, inputCharset, onTagInput), + ITEM_COMMAND("Clear inputs", clearInputValues), + ITEM_BASIC("Try arrows, Enter, Delete")); + +MENU_SCREEN(valuesScreen, valuesItems, + ITEM_BACK(".. Back"), + ITEM_FONT(ITEM_LABEL("Value text and boxes"), u8g2_font_7x13B_tf), + ITEM_VALUE("Uptime", uptimeSeconds, "%lus"), + ITEM_VALUE("Temp", temperatureC, "%.1fC"), + ITEM_VALUE("Voltage", voltage, "%.2fV"), + ITEM_VALUE("RPM", rpm, "%u"), + ITEM_LABEL("Text values"), + ITEM_TOGGLE("Service", "ON", "OFF", onServiceToggle), + ITEM_BOOL_REF("Relay", relayEnabled, "ON", "OFF", onRelayToggle), + ITEM_BOOL("Alarm", false, "ARM", "SAFE", onAlarmToggle), + ITEM_LABEL("Checkbox values"), + ITEM_TOGGLE("Service box", nullptr, nullptr, onServiceToggleBox), + ITEM_BOOL("Alarm box", false, nullptr, nullptr, onAlarmToggleBox), + ITEM_COMMAND("Flip relay externally", flipRelayExternally)); + +MENU_SCREEN(rangeListScreen, rangeListItems, + ITEM_BACK(".. Back"), + ITEM_LABEL("Range / list"), + ITEM_RANGE_REF("Brightness", brightness, 5, 0, 100, onBrightnessChanged, "%d%%", 0, true), + ITEM_RANGE_REF("Volume", volume, 1, 0, 30, onVolumeChanged, "%d", 0, true), + ITEM_LIST("Theme", themes, onThemeChanged, 0, "%s", 0, true), + ITEM_LIST_REF("Profile", profiles, onProfileChanged, profileIndex, "%s", 0, true), + ITEM_COMMAND("Next profile external", nextProfileExternally)); + +MENU_SCREEN(widgetScreen, widgetItems, + ITEM_BACK(".. Back"), + ITEM_FONT(ITEM_LABEL("Multi-widget edit"), u8g2_font_7x13B_tf), + ITEM_WIDGET( + "Schedule", + onScheduleWidget, + WIDGET_RANGE(8, 1, 0, 23, "%02d", 0, true), + WIDGET_RANGE(30, 5, 0, 55, ":%02d", 0, true)), + ITEM_WIDGET( + "Control", + onControlWidget, + WIDGET_LIST(modes, 0, "%s", 0, true), + WIDGET_BOOL(false, "ON", "OFF", " %s")), + ITEM_WIDGET( + "PID", + onPidWidget, + WIDGET_RANGE(10, 1, 0, 99, "P:%02d", 0, true), + WIDGET_RANGE(5, 1, 0, 99, " I:%02d", 0, true), + WIDGET_RANGE(2, 1, 0, 99, " D:%02d", 0, true)), + ITEM_BASIC("List indicator should be visible")); + +MENU_SCREEN(dynamicScreen, dynamicItems, + ITEM_BACK(".. Back"), + ITEM_FONT(ITEM_LABEL("Runtime mutations"), u8g2_font_7x13B_tf), + ITEM_COMMAND("Add item", addDynamicItem), + ITEM_COMMAND("Insert at head", insertDynamicItem), + ITEM_COMMAND("Remove first added", removeDynamicAtHead), + ITEM_COMMAND("Remove last added", removeDynamicLast), + ITEM_COMMAND("Clear all added", clearDynamicItems)); +// clang-format on + +U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE); +U8g2DisplayAdapter display(&u8g2); +GraphicalDisplayRenderer renderer(&display, u8g2_font_6x10_tf); +LcdMenu menu(renderer); +KeyboardAdapter keyboard(&menu, &Serial); + +void setup() { + Serial.begin(115200); + + renderer.begin(); + renderer.setItemFont(navigationItems[1], u8g2_font_7x13B_tf); + renderer.setItemFont(inputItems[1], u8g2_font_7x13B_tf); + renderer.setItemFont(rangeListItems[1], u8g2_font_7x13B_tf); + + dynamicBaseCount = dynamicScreen->size(); + + menu.setScreen(mainScreen); + + Serial.println(F("SSD1306 showcase ready")); + Serial.println(F("Controls: arrows, Enter, Esc, Delete, Backspace")); +} + +void loop() { + keyboard.observe(); + updateTelemetry(); + menu.poll(250); +} + +void refreshMainScreen() { + menu.refresh(); +} + +void jumpToValuesScreen() { + menu.setScreen(valuesScreen); +} + +void onUserInput(char* value) { + Serial.print(F("User: ")); + Serial.println(value); +} + +void onNoteInput(char* value) { + Serial.print(F("Note: ")); + Serial.println(value); +} + +void onTagInput(char* value) { + Serial.print(F("Tag: ")); + Serial.println(value); +} + +void clearInputValues() { + ItemInput* user = static_cast(inputItems[2]); + ItemInput* noteInput = static_cast(inputItems[3]); + ItemInput* tagInput = static_cast(inputItems[4]); + + char* oldUser = user->getValue(); + char* oldNote = noteInput->getValue(); + char* oldTag = tagInput->getValue(); + + user->setValue(cloneText("")); + noteInput->setValue(cloneText("")); + tagInput->setValue(cloneText("")); + + delete[] oldUser; + delete[] oldNote; + delete[] oldTag; + + menu.refresh(); +} + +void onServiceToggle(bool enabled) { + Serial.print(F("Service: ")); + Serial.println(enabled ? F("ON") : F("OFF")); +} + +void onServiceToggleBox(bool enabled) { + Serial.print(F("Service box: ")); + Serial.println(enabled ? F("ON") : F("OFF")); +} + +void onRelayToggle(const Ref enabled) { + Serial.print(F("Relay: ")); + Serial.println(static_cast(enabled) ? F("ON") : F("OFF")); +} + +void onAlarmToggle(const bool enabled) { + Serial.print(F("Alarm: ")); + Serial.println(enabled ? F("ARM") : F("SAFE")); +} + +void onAlarmToggleBox(const bool enabled) { + Serial.print(F("Alarm box: ")); + Serial.println(enabled ? F("ON") : F("OFF")); +} + +void flipRelayExternally() { + relayEnabled = !relayEnabled; + menu.refresh(); +} + +void onBrightnessChanged(const Ref value) { + Serial.print(F("Brightness: ")); + Serial.println(static_cast(value)); +} + +void onVolumeChanged(const Ref value) { + Serial.print(F("Volume: ")); + Serial.println(static_cast(value)); +} + +void onThemeChanged(const uint8_t index) { + if (index < themes.size()) { + Serial.print(F("Theme: ")); + Serial.println(themes[index]); + } +} + +void onProfileChanged(const Ref index) { + uint8_t i = static_cast(index); + if (i < profiles.size()) { + Serial.print(F("Profile: ")); + Serial.println(profiles[i]); + } +} + +void nextProfileExternally() { + profileIndex = static_cast((profileIndex + 1) % profiles.size()); + menu.refresh(); +} + +void onScheduleWidget(int hour, int minute) { + Serial.print(F("Schedule: ")); + if (hour < 10) { + Serial.print('0'); + } + Serial.print(hour); + Serial.print(':'); + if (minute < 10) { + Serial.print('0'); + } + Serial.println(minute); +} + +void onControlWidget(uint8_t modeIndex, bool enabled) { + if (modeIndex < modes.size()) { + Serial.print(F("Mode: ")); + Serial.print(modes[modeIndex]); + Serial.print(F(" / Enabled: ")); + Serial.println(enabled ? F("ON") : F("OFF")); + } +} + +void onPidWidget(int kp, int ki, int kd) { + Serial.print(F("PID: ")); + Serial.print(kp); + Serial.print(' '); + Serial.print(ki); + Serial.print(' '); + Serial.println(kd); +} + +void addDynamicItem() { + char text[24]; + snprintf(text, sizeof(text), "Added #%u", dynamicCounter++); + dynamicScreen->addItem(ITEM_BASIC(cloneText(text))); + menu.refresh(); +} + +void insertDynamicItem() { + char text[24]; + snprintf(text, sizeof(text), "Inserted #%u", dynamicCounter++); + dynamicScreen->addItemAt(dynamicBaseCount, ITEM_BASIC(cloneText(text))); + menu.refresh(); +} + +void removeDynamicAtHead() { + if (dynamicScreen->size() <= dynamicBaseCount) { + return; + } + dynamicScreen->removeItemAt(dynamicBaseCount); + menu.refresh(); +} + +void removeDynamicLast() { + if (dynamicScreen->size() <= dynamicBaseCount) { + return; + } + dynamicScreen->removeLastItem(); + menu.refresh(); +} + +void clearDynamicItems() { + while (dynamicScreen->size() > dynamicBaseCount) { + dynamicScreen->removeLastItem(); + } + menu.refresh(); +} + +void updateTelemetry() { + static unsigned long lastTick = 0; + static int8_t temperatureDirection = 1; + + unsigned long now = millis(); + if (now - lastTick < 1000) { + return; + } + lastTick = now; + + uptimeSeconds++; + temperatureC += 0.2f * temperatureDirection; + if (temperatureC > 28.0f || temperatureC < 21.0f) { + temperatureDirection = -temperatureDirection; + } + voltage = 3.20f + 0.01f * (uptimeSeconds % 20); + rpm = 1100 + (uptimeSeconds % 16) * 40; +} diff --git a/examples/ST7920_SPI/ST7920_SPI.ino b/examples/ST7920_SPI/ST7920_SPI.ino new file mode 100644 index 00000000..00072903 --- /dev/null +++ b/examples/ST7920_SPI/ST7920_SPI.ino @@ -0,0 +1,32 @@ +#include +#include +#include +#include +#include +#include + +// clang-format off +MENU_SCREEN(mainScreen, mainItems, + ITEM_BASIC("Start service"), + ITEM_BASIC("Connect to WiFi"), + ITEM_BASIC("Settings"), + ITEM_BASIC("Blink SOS"), + ITEM_BASIC("Blink random")); +// clang-format on + +U8G2_ST7920_128X64_F_HW_SPI u8g2(U8G2_R0, 10, U8X8_PIN_NONE); +U8g2DisplayAdapter display(&u8g2); +GraphicalDisplayRenderer renderer(&display, u8g2_font_6x10_tf); +LcdMenu menu(renderer); +KeyboardAdapter keyboard(&menu, &Serial); + +void setup() { + Serial.begin(9600); + renderer.begin(); + renderer.setItemFont(mainItems[0], u8g2_font_7x13B_tf); + menu.setScreen(mainScreen); +} + +void loop() { + keyboard.observe(); +} diff --git a/platformio.ini b/platformio.ini index 4217c94c..9911d3f3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -24,6 +24,7 @@ lib_deps = adafruit/DHT sensor library@^1.4.6 adafruit/Adafruit Unified Sensor@^1.1.14 mike-matera/ArduinoSTL@^1.3.3 + olikraus/U8g2@^2.36.7 [env:esp32] platform = espressif32 @@ -38,4 +39,5 @@ lib_deps = Wire sstaub/SSD1803A_I2C@^1.0.2 adafruit/DHT sensor library@^1.4.6 - adafruit/Adafruit Unified Sensor@^1.1.14 \ No newline at end of file + adafruit/Adafruit Unified Sensor@^1.1.14 + olikraus/U8g2@^2.36.7 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/src/display/GraphicalDisplayInterface.h b/src/display/GraphicalDisplayInterface.h new file mode 100644 index 00000000..95a20ef2 --- /dev/null +++ b/src/display/GraphicalDisplayInterface.h @@ -0,0 +1,23 @@ +#pragma once + +#include "DisplayInterface.h" + +/** + * @class GraphicalDisplayInterface + * @brief Interface for pixel-addressable displays. + */ +class GraphicalDisplayInterface : public DisplayInterface { + public: + virtual void setFont(const uint8_t* font) = 0; + virtual uint8_t getDisplayWidth() const = 0; + virtual uint8_t getDisplayHeight() const = 0; + virtual uint8_t getFontWidth() const = 0; + virtual uint8_t getFontHeight() const = 0; + virtual uint8_t getTextWidth(const char* text) = 0; + virtual void setDrawColor(uint8_t color) = 0; + virtual void clearBuffer() = 0; + virtual void sendBuffer() = 0; + virtual void drawBox(uint8_t x, uint8_t y, uint8_t w, uint8_t h) = 0; + virtual void drawFrame(uint8_t x, uint8_t y, uint8_t w, uint8_t h) = 0; + virtual void drawXbm(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t* bitmap) = 0; +}; diff --git a/src/display/U8g2DisplayAdapter.h b/src/display/U8g2DisplayAdapter.h new file mode 100644 index 00000000..54abe293 --- /dev/null +++ b/src/display/U8g2DisplayAdapter.h @@ -0,0 +1,82 @@ +#pragma once + +#include + +#include "GraphicalDisplayInterface.h" + +/** + * @class U8g2DisplayAdapter + * @brief Adapter for displays driven by U8g2. + */ +class U8g2DisplayAdapter : public GraphicalDisplayInterface { + private: + U8G2* u8g2; + uint8_t cursorX = 0; + uint8_t cursorY = 0; + + public: + explicit U8g2DisplayAdapter(U8G2* u8g2) : u8g2(u8g2) {} + + void begin() override { + u8g2->begin(); + u8g2->clearBuffer(); + u8g2->sendBuffer(); + } + + void clear() override { + u8g2->clearBuffer(); + u8g2->sendBuffer(); + } + + void clearBuffer() override { u8g2->clearBuffer(); } + + void sendBuffer() override { u8g2->sendBuffer(); } + + void show() override { u8g2->setPowerSave(0); } + + void hide() override { u8g2->setPowerSave(1); } + + void draw(uint8_t byte) override { + char c[2] = {static_cast(byte), '\0'}; + u8g2->drawUTF8(cursorX, cursorY, c); + cursorX += u8g2->getUTF8Width(c); + } + + void draw(const char* text) override { + u8g2->drawUTF8(cursorX, cursorY, text); + cursorX += u8g2->getUTF8Width(text); + } + + void setCursor(uint8_t col, uint8_t row) override { + cursorX = col; + cursorY = row; + } + + void setBacklight(bool) override {} + + void setDrawColor(uint8_t color) override { u8g2->setDrawColor(color); } + + void setFont(const uint8_t* font) override { u8g2->setFont(font); } + + uint8_t getDisplayWidth() const override { return u8g2->getDisplayWidth(); } + + uint8_t getDisplayHeight() const override { return u8g2->getDisplayHeight(); } + + uint8_t getFontWidth() const override { return u8g2->getMaxCharWidth(); } + + uint8_t getFontHeight() const override { return u8g2->getMaxCharHeight(); } + + uint8_t getTextWidth(const char* text) override { return u8g2->getUTF8Width(text); } + + void drawBox(uint8_t x, uint8_t y, uint8_t w, uint8_t h) override { + u8g2->drawBox(x, y, w, h); + } + + void drawFrame(uint8_t x, uint8_t y, uint8_t w, uint8_t h) override { + u8g2->drawFrame(x, y, w, h); + } + + void drawXbm(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t* bitmap) override { + u8g2->drawXBM(x, y, w, h, bitmap); + } +}; diff --git a/src/renderer/GraphicalDisplayRenderer.cpp b/src/renderer/GraphicalDisplayRenderer.cpp new file mode 100644 index 00000000..fae19013 --- /dev/null +++ b/src/renderer/GraphicalDisplayRenderer.cpp @@ -0,0 +1,663 @@ +#include "GraphicalDisplayRenderer.h" + +#include "MenuItem.h" +#include "renderer/GraphicalMenuItem.h" + +#include + +namespace { +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) { + return 0; + } + size_t len = strlen(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'; + 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* toGraphicalMenuItem(const MenuItem* item) { + if (item == NULL) { + return NULL; + } + const void* capability = item->queryCapability(GraphicalMenuItem::capabilityId()); + return static_cast(capability); +} +} // namespace + +GraphicalDisplayRenderer::GraphicalDisplayRenderer( + GraphicalDisplayInterface* display, + const uint8_t* defaultFont, + const char* cursorIcon, + const char* editCursorIcon) + : MenuRenderer(display, 0, 0), + gDisplay(display), + defaultFont(defaultFont), + cursorIcon(cursorIcon), + editCursorIcon(editCursorIcon) {} + +void GraphicalDisplayRenderer::setDefaultFont(const uint8_t* font) { + defaultFont = font; + + if (defaultFont != NULL) { + gDisplay->setFont(defaultFont); + } + captureCurrentFontMetrics(); + applyItemFont(activeItem); +} + +bool GraphicalDisplayRenderer::setItemFont(MenuItem* item, const uint8_t* font) { + if (!::setItemFont(item, font)) { + return false; + } + + if (font != NULL) { + gDisplay->setFont(font); + captureCurrentFontMetrics(); + } + + applyItemFont(activeItem); + return true; +} + +void GraphicalDisplayRenderer::captureCurrentFontMetrics() { + uint8_t currentHeight = gDisplay->getFontHeight(); + if (currentHeight == 0) { + currentHeight = 8; + } + + uint8_t currentWidth = gDisplay->getFontWidth(); + if (currentWidth == 0) { + currentWidth = 1; + } + + if (currentHeight > maxRowHeight) { + maxRowHeight = currentHeight; + } + if (currentWidth > maxFontWidth) { + maxFontWidth = currentWidth; + } +} + +void GraphicalDisplayRenderer::applyItemFont(const MenuItem* item) { + const uint8_t* selectedFont = defaultFont; + const GraphicalMenuItem* graphicalItem = toGraphicalMenuItem(item); + if (graphicalItem != NULL && graphicalItem->getGraphicalFont() != NULL) { + selectedFont = graphicalItem->getGraphicalFont(); + } + + if (selectedFont != NULL) { + gDisplay->setFont(selectedFont); + } + + captureCurrentFontMetrics(); +} + +void GraphicalDisplayRenderer::begin() { + MenuRenderer::begin(); + + maxRowHeight = 8; + maxFontWidth = 1; + + if (defaultFont != NULL) { + gDisplay->setFont(defaultFont); + } + captureCurrentFontMetrics(); + applyItemFont(activeItem); + + beginFrame(); + endFrame(); +} + +void GraphicalDisplayRenderer::beginFrame() { + gDisplay->clearBuffer(); +} + +void GraphicalDisplayRenderer::endFrame() { + gDisplay->sendBuffer(); +} + +void GraphicalDisplayRenderer::setViewportContext(uint8_t viewStart, uint8_t totalItems) { + this->viewStart = viewStart; + this->totalItems = totalItems; +} + +void GraphicalDisplayRenderer::setValueAreaWidth(uint8_t width) { + valueAreaWidth = width; +} + +void GraphicalDisplayRenderer::setActiveItem(const MenuItem* item) { + activeItem = item; + clearValueSelection(); + applyItemFont(item); +} + +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); + } + if (extensionId == GraphicalIndicatorRenderer::extensionId()) { + return static_cast(this); + } + if (extensionId == GraphicalValueSelectionRenderer::extensionId()) { + return static_cast(this); + } + if (extensionId == GraphicalRendererContext::extensionId()) { + return static_cast(this); + } + return MenuRenderer::queryExtension(extensionId); +} + +const void* GraphicalDisplayRenderer::queryExtension(uint8_t extensionId) const { + if (extensionId == FrameLifecycleRenderer::extensionId()) { + return static_cast(this); + } + if (extensionId == GraphicalIndicatorRenderer::extensionId()) { + return static_cast(this); + } + if (extensionId == GraphicalValueSelectionRenderer::extensionId()) { + return static_cast(this); + } + if (extensionId == GraphicalRendererContext::extensionId()) { + return static_cast(this); + } + return MenuRenderer::queryExtension(extensionId); +} + +void GraphicalDisplayRenderer::draw(uint8_t byte) { + gDisplay->draw(byte); +} + +void GraphicalDisplayRenderer::drawItem(const char* text, const char* value, bool padWithBlanks) { + (void)padWithBlanks; + + uint8_t rowH = rowHeight(); + uint8_t displayWidth = gDisplay->getDisplayWidth(); + uint8_t top = cursorRow * rowH; + + uint8_t fontHeight = gDisplay->getFontHeight(); + if (fontHeight == 0 || fontHeight > rowH) { + fontHeight = rowH; + } + uint8_t baseline = top + (rowH - fontHeight) / 2 + fontHeight - 1; + + bool showScrollBar = totalItems > getMaxRows(); + uint8_t rightInset = showScrollBar ? scrollbarWidth + scrollbarGap : 0; + uint8_t contentRight = displayWidth > rightInset ? displayWidth - rightInset : displayWidth; + + gDisplay->setDrawColor(0); + if (showScrollBar) { + gDisplay->drawBox(0, top, contentRight, rowH); + } else { + gDisplay->drawBox(0, top, displayWidth, rowH); + } + + bool editing = MenuItem::isEditing(); + bool highlightRow = hasFocus && !editing; + if (highlightRow) { + gDisplay->setDrawColor(1); + gDisplay->drawBox(0, top, contentRight, rowH); + gDisplay->setDrawColor(0); + } else { + gDisplay->setDrawColor(1); + } + + 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 = contentRight / 3; + } + + uint8_t maxValueWidth = valueRight > textX ? valueRight - textX : 0; + if (alignedValueWidth > maxValueWidth) { + alignedValueWidth = maxValueWidth; + } + + if (hasValue && alignedValueWidth > 0 && valueRight > alignedValueWidth) { + valueLeft = valueRight - alignedValueWidth; + } + + uint8_t labelRight = !hasValue ? valueRight : (valueLeft > 1 ? valueLeft - 1 : valueLeft); + + 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 (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 drawnValueWidth = 0; + uint8_t valueX = valueLeft; + uint8_t valueShift = 0; + + if (hasValue && alignedValueWidth > 0) { + if (useToggleBox) { + uint8_t boxSize = toggleIndicatorWidth(); + if (boxSize > alignedValueWidth) { + boxSize = alignedValueWidth; + } + + valueX = valueRight > boxSize ? valueRight - boxSize : valueLeft; + uint8_t boxY = top + (rowH > boxSize ? (rowH - boxSize) / 2 : 0); + + 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); + + 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; + + gDisplay->setDrawColor(1); + if (highlightWidth > 0) { + gDisplay->drawBox(highlightX, top, highlightWidth, rowH); + } + + gDisplay->setDrawColor(0); + gDisplay->setCursor(segmentX, baseline); + gDisplay->draw(segmentBuf); + + gDisplay->setDrawColor(1); + } + } + } + } + + 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(); + } +} + +void GraphicalDisplayRenderer::clearBlinker() {} + +void GraphicalDisplayRenderer::drawBlinker() { + if (!MenuItem::isEditing()) { + return; + } + + uint8_t h = rowHeight(); + uint8_t top = cursorPixelY + 1 > h ? cursorPixelY + 1 - h : 0; + + 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 rowH = rowHeight(); + + cursorPixelX = col * charW; + cursorPixelY = row * rowH + rowH - 1; + gDisplay->setCursor(cursorPixelX, cursorPixelY); +} + +void GraphicalDisplayRenderer::drawSubMenuIndicator() { + 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 + (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 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 + (rowH > listGlyphHeight ? (rowH - listGlyphHeight) / 2 : 0); + + gDisplay->setDrawColor(hasFocus && !MenuItem::isEditing() ? 0 : 1); + gDisplay->drawXbm(x, y, listGlyphWidth, listGlyphHeight, updownGlyph); + gDisplay->setDrawColor(1); +} + +uint8_t GraphicalDisplayRenderer::measureText(const char* text) const { + if (text == NULL) { + return 0; + } + return gDisplay->getTextWidth(text); +} + +uint8_t GraphicalDisplayRenderer::toggleIndicatorWidth() const { + uint8_t width = rowHeight() > 4 ? rowHeight() - 4 : rowHeight(); + return width < 3 ? 3 : width; +} + +uint8_t GraphicalDisplayRenderer::rowHeight() const { + return maxRowHeight == 0 ? 8 : maxRowHeight; +} + +uint8_t GraphicalDisplayRenderer::getMaxRows() const { + uint8_t h = rowHeight(); + if (h == 0) { + return 0; + } + return gDisplay->getDisplayHeight() / h; +} + +uint8_t GraphicalDisplayRenderer::getMaxCols() const { + uint8_t w = maxFontWidth; + if (w == 0) { + w = gDisplay->getFontWidth(); + } + if (w == 0) { + w = 1; + } + return gDisplay->getDisplayWidth() / w; +} + +uint8_t GraphicalDisplayRenderer::getEffectiveCols() const { + uint8_t charW = gDisplay->getFontWidth() == 0 ? 1 : gDisplay->getFontWidth(); + + bool showScrollBar = totalItems > getMaxRows(); + uint8_t rightInset = showScrollBar ? scrollbarWidth + scrollbarGap : 0; + + uint8_t usable = gDisplay->getDisplayWidth(); + if (usable <= rightInset + leftPadding) { + return 0; + } + usable -= rightInset + leftPadding; + + uint8_t cols = usable / charW; + uint8_t iconWidth = measureText(cursorIcon); + uint8_t editWidth = measureText(editCursorIcon); + if (editWidth > iconWidth) { + iconWidth = editWidth; + } + uint8_t iconCols = static_cast((iconWidth + charW - 1) / charW); + if (cols > iconCols) { + cols -= iconCols; + } else { + cols = 0; + } + + return cols; +} + +void GraphicalDisplayRenderer::drawScrollBar() { + uint8_t rows = getMaxRows(); + if (rows == 0 || totalItems <= rows) { + return; + } + + uint8_t displayWidth = gDisplay->getDisplayWidth(); + uint16_t areaHeight16 = static_cast(rows) * rowHeight(); + if (areaHeight16 > gDisplay->getDisplayHeight()) { + areaHeight16 = gDisplay->getDisplayHeight(); + } + uint8_t areaHeight = static_cast(areaHeight16); + + uint8_t x = displayWidth - scrollbarWidth; + + gDisplay->setDrawColor(0); + gDisplay->drawBox(x, 0, scrollbarWidth, areaHeight); + gDisplay->setDrawColor(1); + + uint8_t handleHeight = (static_cast(rows) * areaHeight) / totalItems; + if (handleHeight < 2) { + handleHeight = 2; + } + + 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 new file mode 100644 index 00000000..ab4bc321 --- /dev/null +++ b/src/renderer/GraphicalDisplayRenderer.h @@ -0,0 +1,97 @@ +#pragma once + +#include "FrameLifecycleRenderer.h" +#include "GraphicalIndicatorRenderer.h" +#include "GraphicalItemFont.h" +#include "GraphicalRendererContext.h" +#include "GraphicalValueSelectionRenderer.h" +#include "MenuRenderer.h" +#include "display/GraphicalDisplayInterface.h" +#include "utils/std.h" + +/** + * @class GraphicalDisplayRenderer + * @brief Renderer for pixel-addressable displays (for example U8g2-backed). + */ +class GraphicalDisplayRenderer : public MenuRenderer, + public FrameLifecycleRenderer, + public GraphicalIndicatorRenderer, + public GraphicalValueSelectionRenderer, + public GraphicalRendererContext { + private: + GraphicalDisplayInterface* gDisplay; + const uint8_t* defaultFont = NULL; + + const MenuItem* activeItem = NULL; + uint8_t valueAreaWidth = 0; + uint8_t viewStart = 0; + uint8_t totalItems = 0; + + uint8_t cursorPixelX = 0; + 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; + + static const uint8_t scrollbarWidth = 1; + static const uint8_t scrollbarGap = 1; + static const uint8_t leftPadding = 1; + static const uint8_t rightPadding = 1; + static const uint8_t cursorGap = 1; + static const uint8_t listGlyphWidth = 7; + static const uint8_t listGlyphHeight = 8; + static const uint8_t listGap = 1; + static const uint8_t submenuGlyphWidth = 3; + static const uint8_t submenuGlyphHeight = 5; + + void captureCurrentFontMetrics(); + void applyItemFont(const MenuItem* item); + + uint8_t measureText(const char* text) const; + uint8_t toggleIndicatorWidth() const; + uint8_t rowHeight() const; + void drawScrollBar(); + + uint8_t getMaxRows() const override; + uint8_t getMaxCols() const override; + uint8_t getEffectiveCols() const override; + + public: + explicit GraphicalDisplayRenderer( + GraphicalDisplayInterface* display, + const uint8_t* defaultFont = NULL, + const char* cursorIcon = NULL, + const char* editCursorIcon = NULL); + + void setDefaultFont(const uint8_t* font); + bool setItemFont(MenuItem* item, const uint8_t* font); + + void begin() override; + void beginFrame() override; + void endFrame() override; + + void setViewportContext(uint8_t viewStart, uint8_t totalItems) override; + 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; + + void draw(uint8_t byte) override; + void drawItem(const char* text, const char* value, bool padWithBlanks = true) override; + void clearBlinker() override; + void drawBlinker() override; + void moveCursor(uint8_t cursorCol, uint8_t cursorRow) override; + void drawSubMenuIndicator() override; + void drawListIndicator() override; +}; diff --git a/src/renderer/GraphicalIndicatorRenderer.h b/src/renderer/GraphicalIndicatorRenderer.h new file mode 100644 index 00000000..1535854b --- /dev/null +++ b/src/renderer/GraphicalIndicatorRenderer.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +/** + * @brief Optional renderer interface for graphical row indicators. + */ +class GraphicalIndicatorRenderer { + public: + static uint8_t extensionId() { return 3; } + + virtual ~GraphicalIndicatorRenderer() {} + + virtual void drawSubMenuIndicator() = 0; + virtual void drawListIndicator() = 0; +}; diff --git a/src/renderer/GraphicalItemFont.h b/src/renderer/GraphicalItemFont.h new file mode 100644 index 00000000..04a47c2b --- /dev/null +++ b/src/renderer/GraphicalItemFont.h @@ -0,0 +1,40 @@ +#pragma once + +#include "MenuItem.h" +#include "renderer/GraphicalMenuItem.h" + +inline GraphicalMenuItem* asGraphicalMenuItem(MenuItem* item) { + if (item == NULL) { + return NULL; + } + + const void* capability = item->queryCapability(GraphicalMenuItem::capabilityId()); + return const_cast(static_cast(capability)); +} + +inline const GraphicalMenuItem* asGraphicalMenuItem(const MenuItem* item) { + if (item == NULL) { + return NULL; + } + + const void* capability = item->queryCapability(GraphicalMenuItem::capabilityId()); + return static_cast(capability); +} + +inline bool setItemFont(MenuItem* item, const uint8_t* font) { + GraphicalMenuItem* graphicalItem = asGraphicalMenuItem(item); + if (graphicalItem == NULL) { + return false; + } + + graphicalItem->setGraphicalFont(font); + return true; +} + +template +inline T* ITEM_FONT(T* item, const uint8_t* font) { + if (item != NULL) { + setItemFont(static_cast(item), font); + } + return item; +} diff --git a/src/renderer/GraphicalMenuItem.h b/src/renderer/GraphicalMenuItem.h index dc8537f6..6d6fbde5 100644 --- a/src/renderer/GraphicalMenuItem.h +++ b/src/renderer/GraphicalMenuItem.h @@ -8,6 +8,9 @@ class GraphicalDisplayInterface; * @brief Optional capabilities for items rendered on graphical displays. */ class GraphicalMenuItem { + private: + const uint8_t* itemFont = nullptr; + public: static uint8_t capabilityId() { return 1; } @@ -21,4 +24,12 @@ class GraphicalMenuItem { virtual bool hasGraphicalToggle() const { return false; } virtual bool graphicalToggleState() const { return false; } + + virtual bool hasGraphicalListIndicator() const { return false; } + + virtual bool useTightGraphicalSelectionBox() const { return false; } + + virtual void setGraphicalFont(const uint8_t* font) { itemFont = font; } + + virtual const uint8_t* getGraphicalFont() const { return itemFont; } }; 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; +}; From 87024f9c504ffe552e07231946c74912ce7d3fed Mon Sep 17 00:00:00 2001 From: forntoh Date: Thu, 23 Apr 2026 23:25:10 +0200 Subject: [PATCH 2/5] test: align MenuScreen expectations with clamped viewport behavior --- test/MenuScreenTest.cpp | 4 ++-- test/UnselectableItem.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 17267e9a481eaa70d585933280feb6c20a6f42ca Mon Sep 17 00:00:00 2001 From: forntoh Date: Thu, 23 Apr 2026 23:38:15 +0200 Subject: [PATCH 3/5] refactor: slim graphical renderer and restore baseline screen flow --- .github/workflows/compile-arduino.yml | 1 + src/MenuScreen.cpp | 302 +------------ src/renderer/GraphicalDisplayRenderer.cpp | 503 ++++------------------ test/MenuScreenTest.cpp | 4 +- test/UnselectableItem.cpp | 2 +- 5 files changed, 111 insertions(+), 701 deletions(-) diff --git a/.github/workflows/compile-arduino.yml b/.github/workflows/compile-arduino.yml index ed5d6aaf..2a98b522 100644 --- a/.github/workflows/compile-arduino.yml +++ b/.github/workflows/compile-arduino.yml @@ -26,6 +26,7 @@ jobs: - name: SSD1803A_I2C - name: DHT sensor library - name: Adafruit Unified Sensor + - name: U8g2 UNIVERSAL_SKETCH_PATHS: | - examples/Basic diff --git a/src/MenuScreen.cpp b/src/MenuScreen.cpp index 704dcb3e..c5fd05bf 100644 --- a/src/MenuScreen.cpp +++ b/src/MenuScreen.cpp @@ -1,62 +1,13 @@ #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) { @@ -78,11 +29,9 @@ 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; @@ -99,161 +48,50 @@ void MenuScreen::setCursor(MenuRenderer* renderer, uint8_t position) { constrained = backward < 0 ? constrained : static_cast(backward); } } - - uint8_t previousView = view; - uint8_t viewSize = renderer->getMaxRows(); - if (viewSize == 0) { - viewSize = 1; + if (constrained == cursor) { + return; } + 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 < rows && (view + i) < items.size(); i++) { + for (uint8_t i = 0; i < renderer->maxRows && i < items.size(); i++) { MenuItem* item = this->items[view + i]; - if (item == NULL) { - continue; + if (item == nullptr) { + break; } - 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 = - rows > 0 && index == rows - 1 && (view + rows) < items.size(); + renderer->hasHiddenItemsBelow = index == renderer->maxRows - 1 && (view + renderer->maxRows) < items.size(); renderer->hasFocus = cursor == view + index; renderer->cursorRow = index; } bool MenuScreen::process(LcdMenu* menu, const unsigned char command) { MenuRenderer* renderer = menu->getRenderer(); - 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); - } - } - + syncIndicators(cursor - view, renderer); + if (items[cursor]->process(menu, command)) return true; switch (command) { case UP: renderer->viewShift = 0; @@ -266,22 +104,17 @@ 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: - { - uint8_t maxCols = renderer->getMaxCols(); - if (maxCols > 0 && renderer->cursorCol >= maxCols - 1) { - renderer->viewShift++; - draw(renderer); - } - LOG(F("MenuScreen::right"), renderer->viewShift); - return true; + if (renderer->cursorCol >= renderer->maxCols - 1) { + renderer->viewShift++; + draw(renderer); } + LOG(F("MenuScreen::right"), renderer->viewShift); + return true; case LEFT: if (renderer->viewShift > 0) { renderer->viewShift--; @@ -297,23 +130,11 @@ 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) { - 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); - } + setCursor(renderer, cursor - 1); } else if (view > 0) { view--; draw(renderer); @@ -324,24 +145,12 @@ 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) { - 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()) { + setCursor(renderer, cursor + 1); + } else if (view + renderer->maxRows < items.size()) { view++; draw(renderer); } @@ -387,91 +196,20 @@ 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 < rows && (view + i) < items.size(); i++) { + for (uint8_t i = 0; i < renderer->maxRows && (view + i) < items.size(); i++) { MenuItem* item = this->items[view + i]; - if (item == NULL || !item->polling) { - continue; - } - + if (item == nullptr || !item->polling || MenuItem::isEditing()) continue; syncIndicators(i, renderer); - - if (graphicalContext != NULL) { - graphicalContext->setActiveItem(item); - } - item->draw(renderer); redrawn = true; } - if (graphicalContext != NULL) { - graphicalContext->setActiveItem(NULL); - } - + lastPollTime = millis(); return redrawn; } diff --git a/src/renderer/GraphicalDisplayRenderer.cpp b/src/renderer/GraphicalDisplayRenderer.cpp index fae19013..de98a4df 100644 --- a/src/renderer/GraphicalDisplayRenderer.cpp +++ b/src/renderer/GraphicalDisplayRenderer.cpp @@ -6,7 +6,7 @@ #include namespace { -static const uint8_t updownGlyph[] = { +const uint8_t listGlyph[] = { 0x08, 0x1C, 0x3E, @@ -16,67 +16,7 @@ static const uint8_t updownGlyph[] = { 0x08, }; -static const uint8_t textBufferSize = 64; - -uint8_t safeLength(const char* text) { - if (text == NULL) { - return 0; - } - size_t len = strlen(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'; - 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* toGraphicalMenuItem(const MenuItem* item) { +const GraphicalMenuItem* asGraphical(const MenuItem* item) { if (item == NULL) { return NULL; } @@ -93,16 +33,11 @@ GraphicalDisplayRenderer::GraphicalDisplayRenderer( : MenuRenderer(display, 0, 0), gDisplay(display), defaultFont(defaultFont), - cursorIcon(cursorIcon), - editCursorIcon(editCursorIcon) {} + cursorIcon(cursorIcon == NULL ? ">" : cursorIcon), + editCursorIcon(editCursorIcon == NULL ? "*" : editCursorIcon) {} void GraphicalDisplayRenderer::setDefaultFont(const uint8_t* font) { defaultFont = font; - - if (defaultFont != NULL) { - gDisplay->setFont(defaultFont); - } - captureCurrentFontMetrics(); applyItemFont(activeItem); } @@ -110,61 +45,46 @@ bool GraphicalDisplayRenderer::setItemFont(MenuItem* item, const uint8_t* font) if (!::setItemFont(item, font)) { return false; } - - if (font != NULL) { - gDisplay->setFont(font); - captureCurrentFontMetrics(); - } - applyItemFont(activeItem); return true; } void GraphicalDisplayRenderer::captureCurrentFontMetrics() { - uint8_t currentHeight = gDisplay->getFontHeight(); - if (currentHeight == 0) { - currentHeight = 8; - } + uint8_t h = gDisplay->getFontHeight(); + uint8_t w = gDisplay->getFontWidth(); - uint8_t currentWidth = gDisplay->getFontWidth(); - if (currentWidth == 0) { - currentWidth = 1; + if (h == 0) { + h = 8; + } + if (w == 0) { + w = 1; } - if (currentHeight > maxRowHeight) { - maxRowHeight = currentHeight; + if (h > maxRowHeight) { + maxRowHeight = h; } - if (currentWidth > maxFontWidth) { - maxFontWidth = currentWidth; + if (w > maxFontWidth) { + maxFontWidth = w; } } void GraphicalDisplayRenderer::applyItemFont(const MenuItem* item) { - const uint8_t* selectedFont = defaultFont; - const GraphicalMenuItem* graphicalItem = toGraphicalMenuItem(item); + const uint8_t* font = defaultFont; + const GraphicalMenuItem* graphicalItem = asGraphical(item); if (graphicalItem != NULL && graphicalItem->getGraphicalFont() != NULL) { - selectedFont = graphicalItem->getGraphicalFont(); + font = graphicalItem->getGraphicalFont(); } - - if (selectedFont != NULL) { - gDisplay->setFont(selectedFont); + if (font != NULL) { + gDisplay->setFont(font); } - captureCurrentFontMetrics(); } void GraphicalDisplayRenderer::begin() { MenuRenderer::begin(); - maxRowHeight = 8; maxFontWidth = 1; - - if (defaultFont != NULL) { - gDisplay->setFont(defaultFont); - } - captureCurrentFontMetrics(); - applyItemFont(activeItem); - + applyItemFont(NULL); beginFrame(); endFrame(); } @@ -188,7 +108,6 @@ void GraphicalDisplayRenderer::setValueAreaWidth(uint8_t width) { void GraphicalDisplayRenderer::setActiveItem(const MenuItem* item) { activeItem = item; - clearValueSelection(); applyItemFont(item); } @@ -247,257 +166,63 @@ void GraphicalDisplayRenderer::draw(uint8_t byte) { void GraphicalDisplayRenderer::drawItem(const char* text, const char* value, bool padWithBlanks) { (void)padWithBlanks; - uint8_t rowH = rowHeight(); - uint8_t displayWidth = gDisplay->getDisplayWidth(); - uint8_t top = cursorRow * rowH; - + uint8_t h = rowHeight(); + uint8_t yTop = cursorRow * h; uint8_t fontHeight = gDisplay->getFontHeight(); - if (fontHeight == 0 || fontHeight > rowH) { - fontHeight = rowH; - } - uint8_t baseline = top + (rowH - fontHeight) / 2 + fontHeight - 1; + uint8_t baseline = yTop + (h > fontHeight ? (h + fontHeight) / 2 - 1 : h - 1); - bool showScrollBar = totalItems > getMaxRows(); - uint8_t rightInset = showScrollBar ? scrollbarWidth + scrollbarGap : 0; + uint8_t displayWidth = gDisplay->getDisplayWidth(); + uint8_t rightInset = totalItems > getMaxRows() ? scrollbarWidth + scrollbarGap : 0; uint8_t contentRight = displayWidth > rightInset ? displayWidth - rightInset : displayWidth; gDisplay->setDrawColor(0); - if (showScrollBar) { - gDisplay->drawBox(0, top, contentRight, rowH); - } else { - gDisplay->drawBox(0, top, displayWidth, rowH); - } + gDisplay->drawBox(0, yTop, contentRight, h); - bool editing = MenuItem::isEditing(); - bool highlightRow = hasFocus && !editing; - if (highlightRow) { + if (hasFocus && !MenuItem::isEditing()) { gDisplay->setDrawColor(1); - gDisplay->drawBox(0, top, contentRight, rowH); + gDisplay->drawBox(0, yTop, contentRight, h); gDisplay->setDrawColor(0); } else { gDisplay->setDrawColor(1); } - 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 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); } + x += iconWidth + cursorGap; - uint8_t alignedValueWidth = valueAreaWidth; - if (useToggleBox) { - alignedValueWidth = toggleIndicatorWidth(); - } else if (value != NULL && alignedValueWidth == 0) { - alignedValueWidth = contentRight / 3; - } - - uint8_t maxValueWidth = valueRight > textX ? valueRight - textX : 0; - if (alignedValueWidth > maxValueWidth) { - alignedValueWidth = maxValueWidth; - } + const char* label = text == NULL ? "" : text; + gDisplay->setCursor(x, baseline); + gDisplay->draw(label); + x += measureText(label) + 1; - if (hasValue && alignedValueWidth > 0 && valueRight > alignedValueWidth) { - valueLeft = valueRight - alignedValueWidth; - } - - uint8_t labelRight = !hasValue ? valueRight : (valueLeft > 1 ? valueLeft - 1 : valueLeft); - - 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 (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; + if (value != NULL && value[0] != '\0') { + uint8_t valueWidth = valueAreaWidth; + if (valueWidth == 0) { + valueWidth = measureText(value); } - } - - uint8_t drawnValueWidth = 0; - uint8_t valueX = valueLeft; - uint8_t valueShift = 0; - - if (hasValue && alignedValueWidth > 0) { - if (useToggleBox) { - uint8_t boxSize = toggleIndicatorWidth(); - if (boxSize > alignedValueWidth) { - boxSize = alignedValueWidth; - } - - valueX = valueRight > boxSize ? valueRight - boxSize : valueLeft; - uint8_t boxY = top + (rowH > boxSize ? (rowH - boxSize) / 2 : 0); - - 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); - - 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; - - gDisplay->setDrawColor(1); - if (highlightWidth > 0) { - gDisplay->drawBox(highlightX, top, highlightWidth, rowH); - } - - gDisplay->setDrawColor(0); - gDisplay->setCursor(segmentX, baseline); - gDisplay->draw(segmentBuf); - - gDisplay->setDrawColor(1); - } + uint8_t valueRight = contentRight > rightPadding ? contentRight - rightPadding : contentRight; + uint8_t valueX = valueRight > valueWidth ? valueRight - valueWidth : x; + gDisplay->setCursor(valueX, baseline); + gDisplay->draw(value); + } 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; + 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(); } @@ -509,63 +234,44 @@ void GraphicalDisplayRenderer::drawBlinker() { if (!MenuItem::isEditing()) { return; } - uint8_t h = rowHeight(); uint8_t top = cursorPixelY + 1 > h ? cursorPixelY + 1 - h : 0; - - gDisplay->setDrawColor(1); - gDisplay->drawBox(cursorPixelX, top + 1, 1, h > 2 ? h - 2 : 1); + gDisplay->drawBox(cursorPixelX, top, 1, h); } void GraphicalDisplayRenderer::moveCursor(uint8_t col, uint8_t row) { MenuRenderer::moveCursor(col, row); - uint8_t charW = gDisplay->getFontWidth() == 0 ? 1 : gDisplay->getFontWidth(); - uint8_t rowH = rowHeight(); - + uint8_t h = rowHeight(); cursorPixelX = col * charW; - cursorPixelY = row * rowH + rowH - 1; + cursorPixelY = row * h + h - 1; gDisplay->setCursor(cursorPixelX, cursorPixelY); } void GraphicalDisplayRenderer::drawSubMenuIndicator() { - uint8_t rowH = rowHeight(); - uint8_t top = cursorRow * rowH; - - bool showScrollBar = totalItems > getMaxRows(); - uint8_t rightInset = showScrollBar ? scrollbarWidth + scrollbarGap : 0; + 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 x = contentRight > rightPadding + submenuGlyphWidth ? contentRight - rightPadding - submenuGlyphWidth : leftPadding; - uint8_t y = top + (rowH > submenuGlyphHeight ? (rowH - submenuGlyphHeight) / 2 : 0); - - gDisplay->setDrawColor(hasFocus && !MenuItem::isEditing() ? 0 : 1); - + uint8_t y = top + (h > submenuGlyphHeight ? (h - submenuGlyphHeight) / 2 : 0); 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 rowH = rowHeight(); - uint8_t top = cursorRow * rowH; - - bool showScrollBar = totalItems > getMaxRows(); - uint8_t rightInset = showScrollBar ? scrollbarWidth + scrollbarGap : 0; + 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 x = contentRight > rightPadding + listGlyphWidth ? contentRight - rightPadding - listGlyphWidth : leftPadding; - uint8_t y = top + (rowH > listGlyphHeight ? (rowH - listGlyphHeight) / 2 : 0); - - gDisplay->setDrawColor(hasFocus && !MenuItem::isEditing() ? 0 : 1); - gDisplay->drawXbm(x, y, listGlyphWidth, listGlyphHeight, updownGlyph); - gDisplay->setDrawColor(1); + uint8_t y = top + (h > listGlyphHeight ? (h - listGlyphHeight) / 2 : 0); + gDisplay->drawXbm(x, y, listGlyphWidth, listGlyphHeight, listGlyph); } uint8_t GraphicalDisplayRenderer::measureText(const char* text) const { @@ -576,59 +282,32 @@ uint8_t GraphicalDisplayRenderer::measureText(const char* text) const { } uint8_t GraphicalDisplayRenderer::toggleIndicatorWidth() const { - uint8_t width = rowHeight() > 4 ? rowHeight() - 4 : rowHeight(); - return width < 3 ? 3 : width; + uint8_t h = rowHeight(); + return h > 4 ? h - 4 : h; } uint8_t GraphicalDisplayRenderer::rowHeight() const { - return maxRowHeight == 0 ? 8 : maxRowHeight; + return maxRowHeight == 0 ? 8 : static_cast(maxRowHeight + 2); } uint8_t GraphicalDisplayRenderer::getMaxRows() const { uint8_t h = rowHeight(); - if (h == 0) { - return 0; - } - return gDisplay->getDisplayHeight() / h; + return h == 0 ? 0 : gDisplay->getDisplayHeight() / h; } uint8_t GraphicalDisplayRenderer::getMaxCols() const { - uint8_t w = maxFontWidth; - if (w == 0) { - w = gDisplay->getFontWidth(); - } - if (w == 0) { - w = 1; - } + uint8_t w = maxFontWidth == 0 ? 1 : maxFontWidth; return gDisplay->getDisplayWidth() / w; } uint8_t GraphicalDisplayRenderer::getEffectiveCols() const { - uint8_t charW = gDisplay->getFontWidth() == 0 ? 1 : gDisplay->getFontWidth(); - - bool showScrollBar = totalItems > getMaxRows(); - uint8_t rightInset = showScrollBar ? scrollbarWidth + scrollbarGap : 0; - - uint8_t usable = gDisplay->getDisplayWidth(); - if (usable <= rightInset + leftPadding) { - return 0; - } - usable -= rightInset + leftPadding; - - uint8_t cols = usable / charW; - uint8_t iconWidth = measureText(cursorIcon); - uint8_t editWidth = measureText(editCursorIcon); - if (editWidth > iconWidth) { - iconWidth = editWidth; - } - uint8_t iconCols = static_cast((iconWidth + charW - 1) / charW); - if (cols > iconCols) { - cols -= iconCols; - } else { - cols = 0; + uint8_t w = gDisplay->getFontWidth(); + if (w == 0) { + w = 1; } - - return cols; + uint8_t rightInset = totalItems > getMaxRows() ? scrollbarWidth + scrollbarGap : 0; + uint8_t usable = gDisplay->getDisplayWidth() > rightInset ? gDisplay->getDisplayWidth() - rightInset : 0; + return usable / w; } void GraphicalDisplayRenderer::drawScrollBar() { @@ -637,27 +316,19 @@ void GraphicalDisplayRenderer::drawScrollBar() { return; } - uint8_t displayWidth = gDisplay->getDisplayWidth(); - uint16_t areaHeight16 = static_cast(rows) * rowHeight(); - if (areaHeight16 > gDisplay->getDisplayHeight()) { - areaHeight16 = gDisplay->getDisplayHeight(); + uint8_t x = gDisplay->getDisplayWidth() - scrollbarWidth; + uint8_t areaHeight = rows * rowHeight(); + if (areaHeight > gDisplay->getDisplayHeight()) { + areaHeight = gDisplay->getDisplayHeight(); } - uint8_t areaHeight = static_cast(areaHeight16); - - uint8_t x = displayWidth - scrollbarWidth; - - gDisplay->setDrawColor(0); - gDisplay->drawBox(x, 0, scrollbarWidth, areaHeight); - gDisplay->setDrawColor(1); uint8_t handleHeight = (static_cast(rows) * areaHeight) / totalItems; if (handleHeight < 2) { handleHeight = 2; } - uint16_t trackRange = areaHeight > handleHeight ? areaHeight - handleHeight : 0; - uint16_t scrollRange = totalItems > rows ? totalItems - rows : 1; - uint8_t y = (static_cast(viewStart) * trackRange) / scrollRange; - + uint16_t scrollRange = totalItems - rows; + uint16_t trackRange = areaHeight - handleHeight; + uint8_t y = scrollRange == 0 ? 0 : (static_cast(viewStart) * trackRange) / scrollRange; gDisplay->drawBox(x, y, scrollbarWidth, handleHeight); } diff --git a/test/MenuScreenTest.cpp b/test/MenuScreenTest.cpp index 5a4432b2..70feb62b 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)1, i2->drawCount); - assertEqual((uint8_t)1, i3->drawCount); + assertEqual((uint8_t)0, i2->drawCount); + assertEqual((uint8_t)0, i3->drawCount); // no items drawn but no crash delete i1; delete i2; diff --git a/test/UnselectableItem.cpp b/test/UnselectableItem.cpp index 2f5dde96..9818f625 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)0, screen.view); + assertEqual((uint8_t)1, screen.view); delete label; delete first; delete second; From 6234a281054b2a44f24fc4e767dd13748e9f2df9 Mon Sep 17 00:00:00 2001 From: forntoh Date: Thu, 23 Apr 2026 23:41:38 +0200 Subject: [PATCH 4/5] refactor: trim graphical renderer extension surface --- src/renderer/GraphicalDisplayRenderer.cpp | 36 +++---------------- src/renderer/GraphicalDisplayRenderer.h | 9 ----- .../GraphicalValueSelectionRenderer.h | 16 --------- 3 files changed, 4 insertions(+), 57 deletions(-) delete mode 100644 src/renderer/GraphicalValueSelectionRenderer.h diff --git a/src/renderer/GraphicalDisplayRenderer.cpp b/src/renderer/GraphicalDisplayRenderer.cpp index de98a4df..605afccd 100644 --- a/src/renderer/GraphicalDisplayRenderer.cpp +++ b/src/renderer/GraphicalDisplayRenderer.cpp @@ -6,15 +6,7 @@ #include namespace { -const uint8_t listGlyph[] = { - 0x08, - 0x1C, - 0x3E, - 0x00, - 0x3E, - 0x1C, - 0x08, -}; +const uint8_t listGlyph[] = {0x08, 0x1C, 0x3E, 0x00, 0x3E, 0x1C, 0x08}; const GraphicalMenuItem* asGraphical(const MenuItem* item) { if (item == NULL) { @@ -115,18 +107,6 @@ 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); @@ -134,9 +114,6 @@ 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); } @@ -150,9 +127,6 @@ 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); } @@ -252,8 +226,7 @@ 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 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); gDisplay->drawBox(x, y, 1, 1); @@ -267,8 +240,7 @@ 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 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); gDisplay->drawXbm(x, y, listGlyphWidth, listGlyphHeight, listGlyph); @@ -287,7 +259,7 @@ uint8_t GraphicalDisplayRenderer::toggleIndicatorWidth() const { } uint8_t GraphicalDisplayRenderer::rowHeight() const { - return maxRowHeight == 0 ? 8 : static_cast(maxRowHeight + 2); + return maxRowHeight == 0 ? 8 : maxRowHeight + 2; } uint8_t GraphicalDisplayRenderer::getMaxRows() const { diff --git a/src/renderer/GraphicalDisplayRenderer.h b/src/renderer/GraphicalDisplayRenderer.h index ab4bc321..061323e7 100644 --- a/src/renderer/GraphicalDisplayRenderer.h +++ b/src/renderer/GraphicalDisplayRenderer.h @@ -4,7 +4,6 @@ #include "GraphicalIndicatorRenderer.h" #include "GraphicalItemFont.h" #include "GraphicalRendererContext.h" -#include "GraphicalValueSelectionRenderer.h" #include "MenuRenderer.h" #include "display/GraphicalDisplayInterface.h" #include "utils/std.h" @@ -16,7 +15,6 @@ class GraphicalDisplayRenderer : public MenuRenderer, public FrameLifecycleRenderer, public GraphicalIndicatorRenderer, - public GraphicalValueSelectionRenderer, public GraphicalRendererContext { private: GraphicalDisplayInterface* gDisplay; @@ -32,10 +30,6 @@ class GraphicalDisplayRenderer : public MenuRenderer, 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; @@ -81,9 +75,6 @@ class GraphicalDisplayRenderer : public MenuRenderer, 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/src/renderer/GraphicalValueSelectionRenderer.h b/src/renderer/GraphicalValueSelectionRenderer.h deleted file mode 100644 index be66c14b..00000000 --- a/src/renderer/GraphicalValueSelectionRenderer.h +++ /dev/null @@ -1,16 +0,0 @@ -#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; -}; From 46bb13ae90cd4d59aa6f85788576ac6980fcbed9 Mon Sep 17 00:00:00 2001 From: forntoh Date: Fri, 24 Apr 2026 00:11:19 +0200 Subject: [PATCH 5/5] fix: align list glyph bitmap height with drawXbm --- src/renderer/GraphicalDisplayRenderer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/GraphicalDisplayRenderer.cpp b/src/renderer/GraphicalDisplayRenderer.cpp index 605afccd..fbe05814 100644 --- a/src/renderer/GraphicalDisplayRenderer.cpp +++ b/src/renderer/GraphicalDisplayRenderer.cpp @@ -6,7 +6,7 @@ #include namespace { -const uint8_t listGlyph[] = {0x08, 0x1C, 0x3E, 0x00, 0x3E, 0x1C, 0x08}; +const uint8_t listGlyph[] = {0x08, 0x1C, 0x3E, 0x00, 0x3E, 0x1C, 0x08, 0x00}; const GraphicalMenuItem* asGraphical(const MenuItem* item) { if (item == NULL) {