Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3bdd643
Add ST7920 SPI display support
forntoh Jun 15, 2025
1d3435e
Generalize U8g2 display adapter
forntoh Jun 15, 2025
1f3d411
Add SSD1306 graphical display example
forntoh Jun 15, 2025
146cba2
Flush graphical display buffer and add ST7920 example
forntoh Jun 15, 2025
c89b8ad
Flush buffer for graphical updates
forntoh Jun 15, 2025
316686f
Refine buffer flushing for graphical renderer
forntoh Jun 15, 2025
437fe20
Use font-based width for graphical rendering
forntoh Jun 15, 2025
a6e1c8c
Update README for graphical display support
forntoh Jun 15, 2025
e309458
Allow UTF-8 cursor icons for graphical renderer
forntoh Jun 15, 2025
08b9802
Use UTF8 APIs in U8g2 adapter
forntoh Jun 15, 2025
a8728c5
Add submenu arrows and highlight style
forntoh Jun 16, 2025
82d3a62
Refine graphical renderer layout
forntoh Jun 16, 2025
e82307d
Refine graphical renderer metrics
forntoh Jun 16, 2025
bd8903a
defer dimension calc for graphical renderer
forntoh Jun 16, 2025
9d45cc4
compute rows and cols dynamically
forntoh Jun 16, 2025
47ed19a
Refine graphical renderer layout
forntoh Jun 16, 2025
564e5af
Set default font for graphical renderer
forntoh Jun 16, 2025
6e25bdf
Refine graphical renderer alignment
forntoh Jun 16, 2025
cd29109
Adjust value alignment and scrollbar margin
forntoh Jun 16, 2025
901ef74
Add list indicator glyph for graphical displays
forntoh Jun 16, 2025
546e971
Fix list detection and improve cursor icon handling
forntoh Jun 16, 2025
c656912
fix: restore colon and cursor icons
forntoh Jun 16, 2025
c2b98fa
Fix graphical glyph rendering and update docs
forntoh Jun 16, 2025
0bf0bb1
Fix list indicator alignment
forntoh Jun 16, 2025
bc66658
Improve graphical renderer editing highlights
forntoh Jun 16, 2025
9681dc6
Fix list glyph position and value highlighting
forntoh Jun 16, 2025
e28b532
Fix edit highlight to only cover active value
forntoh Jun 16, 2025
88c4b95
Fix scroll bar height and position
forntoh Jun 16, 2025
938c889
Highlight entire value for multi-widget items
forntoh Jun 16, 2025
fdd4df4
Remove value highlight and simplify widget drawing
forntoh Jun 16, 2025
4f3eb04
Fix list indicator and restore lastValue param
forntoh Jun 16, 2025
d9f09e7
Update buffer handling
forntoh Jun 16, 2025
b945f27
Clear display buffer before drawing menu items
forntoh Jun 16, 2025
ead4383
Merge branch 'master' into codex/add-support-for-128x64-spi-lcd
forntoh Jun 16, 2025
6b1aacd
Implement measureValueWidth for single-value items
forntoh Jun 16, 2025
10569d6
Merge branch 'master' into codex/add-support-for-128x64-spi-lcd
forntoh Jun 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 driven by the U8g2 library, such as SSD1306 or ST7920 modules.

<p align="center">
<img src="https://i.imgur.com/nViET8b.gif" alt="Example of a menu system created with LcdMenu">
Expand Down
31 changes: 31 additions & 0 deletions docs/source/overview/rendering/graphical-display.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Graphical display renderer
=========================

The graphical display renderer works with dot-matrix displays using the
:cpp:class:`GraphicalDisplayInterface`. It is ideal for OLED or graphical
LCD modules driven by the `U8g2` library, including classic ST7920 128x64
SPI LCDs.

Features
--------

* Scalable font rendering using the active U8g2 font
* Scroll bar indicator instead of arrow icons
* Customizable cursor characters

Basic usage
-----------

.. code-block:: cpp

#include <U8g2lib.h>
#include <display/U8g2DisplayAdapter.h>
#include <renderer/GraphicalDisplayRenderer.h>

U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
U8g2DisplayAdapter lcdAdapter(&u8g2);
GraphicalDisplayRenderer renderer(&lcdAdapter, u8g2_font_6x10_tf);

void setup() {
renderer.begin();
}
3 changes: 2 additions & 1 deletion docs/source/overview/rendering/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand All @@ -24,4 +25,4 @@ Here are some that would be cool to have:
- Serial renderer
- Web renderer
- TFT renderer
- OLED renderer
- OLED renderer
29 changes: 29 additions & 0 deletions examples/SSD1306_I2C/SSD1306_I2C.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#include <LcdMenu.h>
#include <MenuScreen.h>
#include <U8g2lib.h>
#include <display/U8g2DisplayAdapter.h>
#include <input/KeyboardAdapter.h>
#include <renderer/GraphicalDisplayRenderer.h>

// 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_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
U8g2DisplayAdapter lcdAdapter(&u8g2);
GraphicalDisplayRenderer renderer(&lcdAdapter, u8g2_font_6x10_tf);
LcdMenu menu(renderer);
KeyboardAdapter keyboard(&menu, &Serial);

void setup() {
Serial.begin(9600);
renderer.begin();
menu.setScreen(mainScreen);
}

void loop() { keyboard.observe(); }
29 changes: 29 additions & 0 deletions examples/ST7920_SPI/ST7920_SPI.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#include <LcdMenu.h>
#include <MenuScreen.h>
#include <U8g2lib.h>
#include <display/U8g2DisplayAdapter.h>
#include <input/KeyboardAdapter.h>
#include <renderer/GraphicalDisplayRenderer.h>

// 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 lcdAdapter(&u8g2);
GraphicalDisplayRenderer renderer(&lcdAdapter, u8g2_font_6x10_tf);
LcdMenu menu(renderer);
KeyboardAdapter keyboard(&menu, &Serial);

void setup() {
Serial.begin(9600);
renderer.begin();
menu.setScreen(mainScreen);
}

void loop() { keyboard.observe(); }
6 changes: 4 additions & 2 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ lib_deps =
sstaub/SSD1803A_I2C@^1.0.2
adafruit/DHT sensor library@^1.4.6
adafruit/Adafruit Unified Sensor@^1.1.14
mike-matera/ArduinoSTL@^1.3.3
mike-matera/ArduinoSTL@^1.3.3
olikraus/U8g2@^2.36.7

[env:esp32]
platform = espressif32
Expand All @@ -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
adafruit/Adafruit Unified Sensor@^1.1.14
olikraus/U8g2@^2.36.7
17 changes: 13 additions & 4 deletions src/BaseItemManyWidgets.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ class BaseItemManyWidgets : public MenuItem {
}
}

uint8_t measureValueWidth(GraphicalDisplayInterface* display) override {
if (!display) return 0;
char buf[ITEM_DRAW_BUFFER_SIZE];
uint8_t index = 0;
for (auto* w : widgets)
index += w->draw(buf, index);
buf[index] = '\0';
return display->getTextWidth(buf);
}

virtual ~BaseItemManyWidgets() {
for (auto widget : widgets) {
delete widget;
Expand Down Expand Up @@ -111,21 +121,20 @@ class BaseItemManyWidgets : public MenuItem {

uint8_t index = 0;
uint8_t cursorCol = 0;
bool hasListWidget = false;

for (uint8_t i = 0; i < widgets.size(); i++) {
index += widgets[i]->draw(buf, index);
if (widgets[i]->isList()) hasListWidget = true;
if (i == activeWidget && renderer->isInEditMode()) {
// Calculate the available space for the widgets after the text
size_t v_size = renderer->getEffectiveCols() - strlen(text) - 1;
// Adjust the view shift to ensure the active widget is visible
renderer->viewShift = index > v_size ? index - v_size : 0;
// Draw the item with the renderer, indicating if it's the last widget
renderer->drawItem(text, buf, i == widgets.size() - 1);
// Calculate the cursor column position for the active widget
cursorCol = renderer->getCursorCol() - 1 - widgets[i]->cursorOffset;
}
}
renderer->drawItem(text, buf);
if (hasListWidget) renderer->drawListIndicator();

if (renderer->isInEditMode()) {
renderer->moveCursor(cursorCol, renderer->getCursorRow());
Expand Down
5 changes: 5 additions & 0 deletions src/ItemInput.h
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ class ItemInput : public MenuItem {
}
return false;
}

uint8_t measureValueWidth(GraphicalDisplayInterface* display) override {
if (!display) return 0;
return display->getTextWidth(value);
}
/**
* Get the callback function for this item.
*
Expand Down
5 changes: 5 additions & 0 deletions src/ItemSubMenu.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ class ItemSubMenu : public BaseItemZeroWidget {
}

protected:
void draw(MenuRenderer* renderer) override {
renderer->drawItem(text, nullptr);
renderer->drawSubMenuIndicator();
}

void handleCommit(LcdMenu* menu) override {
LOG(F("ItemSubMenu::changeScreen"), text);
screen->setParent(menu->getScreen());
Expand Down
7 changes: 7 additions & 0 deletions src/ItemToggle.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ class ItemToggle : public MenuItem {

const char* getTextOff() { return this->textOff; }

uint8_t measureValueWidth(GraphicalDisplayInterface* display) override {
if (!display) return 0;
uint8_t wOn = display->getTextWidth(textOn ? textOn : "");
uint8_t wOff = display->getTextWidth(textOff ? textOff : "");
return wOn > wOff ? wOn : wOff;
}

void draw(MenuRenderer* renderer) override {
renderer->drawItem(text, enabled ? textOn : textOff);
};
Expand Down
7 changes: 7 additions & 0 deletions src/ItemValue.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ class ItemValue : public BaseItemZeroWidget {
snprintf(buffer, ITEM_DRAW_BUFFER_SIZE, format, value);
renderer->drawItem(text, buffer);
}

uint8_t measureValueWidth(GraphicalDisplayInterface* display) override {
if (!display) return 0;
char buffer[ITEM_DRAW_BUFFER_SIZE];
snprintf(buffer, ITEM_DRAW_BUFFER_SIZE, format, value);
return display->getTextWidth(buffer);
}
};

/**
Expand Down
13 changes: 12 additions & 1 deletion src/LcdMenu.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "LcdMenu.h"
#include "display/GraphicalDisplayInterface.h"

MenuRenderer* LcdMenu::getRenderer() {
return &renderer;
Expand All @@ -13,14 +14,20 @@ void LcdMenu::setScreen(MenuScreen* screen) {
this->screen = screen;
renderer.display->clear();
this->screen->reset(&renderer);
this->screen->draw(&renderer);
if (renderer.display->isGraphical())
static_cast<GraphicalDisplayInterface*>(renderer.display)->sendBuffer();
}

bool LcdMenu::process(const unsigned char c) {
if (!enabled) {
return false;
}
renderer.restartTimer();
return screen->process(this, c);
bool handled = screen->process(this, c);
if (handled && renderer.display->isGraphical())
static_cast<GraphicalDisplayInterface*>(renderer.display)->sendBuffer();
return handled;
};

void LcdMenu::reset() {
Expand All @@ -42,6 +49,8 @@ void LcdMenu::show() {
enabled = true;
renderer.display->clear();
screen->draw(&renderer);
if (renderer.display->isGraphical())
static_cast<GraphicalDisplayInterface*>(renderer.display)->sendBuffer();
}

uint8_t LcdMenu::getCursor() {
Expand All @@ -64,6 +73,8 @@ void LcdMenu::refresh() {
return;
}
screen->draw(&renderer);
if (renderer.display->isGraphical())
static_cast<GraphicalDisplayInterface*>(renderer.display)->sendBuffer();
}

void LcdMenu::poll(uint16_t pollInterval) {
Expand Down
10 changes: 10 additions & 0 deletions src/MenuItem.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

#define ITEM_DRAW_BUFFER_SIZE 25

#include "display/GraphicalDisplayInterface.h"
#include "renderer/MenuRenderer.h"
#include "utils/constants.h"
#include <utils/utils.h>
Expand Down Expand Up @@ -79,6 +80,15 @@ class MenuItem {
// Destructor
virtual ~MenuItem() noexcept = default;

/**
* @brief Measure the width of this item's value using the given display.
* @param display Optional graphical display for width calculation.
* @return Width in pixels of the value, or 0 if the item has none.
*/
virtual uint8_t measureValueWidth(GraphicalDisplayInterface* display) {
return 0;
}

protected:
/**
* @brief The number of available columns for the potential value of the item.
Expand Down
36 changes: 28 additions & 8 deletions src/MenuScreen.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#include "MenuScreen.h"
#include "display/GraphicalDisplayInterface.h"
#include "renderer/GraphicalDisplayRenderer.h"

void MenuScreen::setParent(MenuScreen* parent) {
this->parent = parent;
Expand Down Expand Up @@ -41,7 +43,7 @@ void MenuScreen::setCursor(MenuRenderer* renderer, uint8_t position) {
if (constrained == cursor) {
return;
}
uint8_t viewSize = renderer->maxRows;
uint8_t viewSize = renderer->getMaxRows();
if (constrained < view) {
view = constrained;
} else if (constrained > (view + (viewSize - 1))) {
Expand All @@ -52,7 +54,25 @@ void MenuScreen::setCursor(MenuRenderer* renderer, uint8_t position) {
}

void MenuScreen::draw(MenuRenderer* renderer) {
for (uint8_t i = 0; i < renderer->maxRows && i < items.size(); i++) {
GraphicalDisplayInterface* gDisplay =
renderer->display->isGraphical()
? static_cast<GraphicalDisplayInterface*>(renderer->display)
: nullptr;
GraphicalDisplayRenderer* gRenderer =
gDisplay ? static_cast<GraphicalDisplayRenderer*>(renderer) : nullptr;
if (gRenderer) {
gDisplay->clearBuffer();
uint8_t widest = 0;
for (uint8_t i = 0; i < renderer->getMaxRows() && (view + i) < items.size(); i++) {
MenuItem* it = items[view + i];
uint8_t w = it->measureValueWidth(gDisplay);
if (w > widest) widest = w;
}
uint8_t maxAllowed = gDisplay->getDisplayWidth() / 2;
if (widest > maxAllowed) widest = maxAllowed;
gRenderer->setValueWidth(widest);
}
for (uint8_t i = 0; i < renderer->getMaxRows() && (view + i) < items.size(); i++) {
MenuItem* item = this->items[view + i];
if (item == nullptr) {
break;
Expand All @@ -64,9 +84,11 @@ void MenuScreen::draw(MenuRenderer* renderer) {

void MenuScreen::syncIndicators(uint8_t index, MenuRenderer* renderer) {
renderer->hasHiddenItemsAbove = index == 0 && view > 0;
renderer->hasHiddenItemsBelow = index == renderer->maxRows - 1 && (view + renderer->maxRows) < items.size();
renderer->hasHiddenItemsBelow = index == renderer->getMaxRows() - 1 && (view + renderer->getMaxRows()) < items.size();
renderer->hasFocus = cursor == view + index;
renderer->cursorRow = index;
renderer->viewStart = view;
renderer->totalItems = items.size();
}

bool MenuScreen::process(LcdMenu* menu, const unsigned char command) {
Expand All @@ -90,7 +112,7 @@ bool MenuScreen::process(LcdMenu* menu, const unsigned char command) {
LOG(F("MenuScreen::back"));
return true;
case RIGHT:
if (renderer->cursorCol >= renderer->maxCols - 1) {
if (renderer->cursorCol >= renderer->getMaxCols() - 1) {
renderer->viewShift++;
draw(renderer);
}
Expand Down Expand Up @@ -130,9 +152,7 @@ void MenuScreen::down(MenuRenderer* renderer) {
return;
}
if (cursor < items.size() - 1) {
setCursor(renderer, cursor + 1);
} else if (view + renderer->maxRows < items.size()) {
view++;
if (++cursor > view + renderer->getMaxRows() - 1) view++;
draw(renderer);
}
LOG(F("MenuScreen::down"), cursor);
Expand Down Expand Up @@ -179,7 +199,7 @@ void MenuScreen::clear() {
void MenuScreen::poll(MenuRenderer* renderer, uint16_t pollInterval) {
static unsigned long lastPollTime = 0;
if (millis() - lastPollTime >= pollInterval) {
for (uint8_t i = 0; i < renderer->maxRows && (view + i) < items.size(); i++) {
for (uint8_t i = 0; i < renderer->getMaxRows() && (view + i) < items.size(); i++) {
MenuItem* item = this->items[view + i];
if (item == nullptr || !item->polling || renderer->isInEditMode()) continue;
syncIndicators(i, renderer);
Expand Down
5 changes: 3 additions & 2 deletions src/display/DisplayInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ class DisplayInterface {
virtual void clear() = 0;
virtual void show() = 0;
virtual void hide() = 0;
virtual void draw(uint8_t byte) = 0;
virtual void draw(const char* text) = 0;
virtual uint8_t draw(uint8_t byte) = 0;
virtual uint8_t draw(const char* text) = 0;
virtual void setCursor(uint8_t col, uint8_t row) = 0;
virtual void setBacklight(bool enabled) = 0;
virtual bool isGraphical() const { return false; }
virtual ~DisplayInterface() {}
};

Expand Down
Loading
Loading