Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions docs/source/overview/rendering/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,14 @@ Here are some that would be cool to have:

CI compiles hardware-specific rendering examples only on compatible board
jobs. For example, ``examples/ST7920_SPI`` is compiled on ESP32 jobs.

Renderer extension hooks
------------------------

Advanced renderers can expose optional capabilities through
``MenuRenderer::queryExtension()``. Menu items can similarly expose optional
capabilities through ``MenuItem::queryCapability()``.

This extension model keeps the base APIs small for character displays, while
still allowing specialized renderers to opt into features such as explicit
frame lifecycle hooks.
35 changes: 33 additions & 2 deletions src/LcdMenu.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
#include "LcdMenu.h"
#include "renderer/FrameLifecycleRenderer.h"

namespace {
FrameLifecycleRenderer* frameLifecycle(MenuRenderer& renderer) {
return static_cast<FrameLifecycleRenderer*>(renderer.queryExtension(FrameLifecycleRenderer::extensionId()));
}
} // namespace

MenuRenderer* LcdMenu::getRenderer() {
return &renderer;
Expand All @@ -13,14 +20,28 @@ void LcdMenu::setScreen(MenuScreen* screen) {
this->screen = screen;
renderer.display->clear();
this->screen->reset(&renderer);

FrameLifecycleRenderer* frame = frameLifecycle(renderer);
if (frame != NULL) {
frame->endFrame();
}
}

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) {
FrameLifecycleRenderer* frame = frameLifecycle(renderer);
if (frame != NULL) {
frame->endFrame();
}
}

return handled;
};

void LcdMenu::reset() {
Expand All @@ -42,6 +63,11 @@ void LcdMenu::show() {
enabled = true;
renderer.display->clear();
screen->draw(&renderer);

FrameLifecycleRenderer* frame = frameLifecycle(renderer);
if (frame != NULL) {
frame->endFrame();
}
}

uint8_t LcdMenu::getCursor() {
Expand All @@ -64,6 +90,11 @@ void LcdMenu::refresh() {
return;
}
screen->draw(&renderer);

FrameLifecycleRenderer* frame = frameLifecycle(renderer);
if (frame != NULL) {
frame->endFrame();
}
}

void LcdMenu::poll(uint16_t pollInterval) {
Expand All @@ -74,4 +105,4 @@ void LcdMenu::poll(uint16_t pollInterval) {
}
bool LcdMenu::isEnabled() const {
return enabled;
}
}
10 changes: 10 additions & 0 deletions src/MenuItem.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ class MenuItem {
// Destructor
virtual ~MenuItem() noexcept = default;

/**
* @brief Optional capability lookup for item-specific extensions.
*
* Returns NULL when capability is not supported.
*/
virtual const void* queryCapability(uint8_t capabilityId) const {
(void)capabilityId;
return NULL;
}

protected:
/**
* @brief The number of available columns for the potential value of the item.
Expand Down
15 changes: 15 additions & 0 deletions src/MenuScreen.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
#include "MenuScreen.h"
#include "renderer/FrameLifecycleRenderer.h"

namespace {
FrameLifecycleRenderer* toFrameLifecycle(MenuRenderer* renderer) {
if (renderer == NULL) {
return NULL;
}
return static_cast<FrameLifecycleRenderer*>(renderer->queryExtension(FrameLifecycleRenderer::extensionId()));
}
} // namespace

void MenuScreen::setParent(MenuScreen* parent) {
this->parent = parent;
Expand Down Expand Up @@ -52,6 +62,11 @@ void MenuScreen::setCursor(MenuRenderer* renderer, uint8_t position) {
}

void MenuScreen::draw(MenuRenderer* renderer) {
FrameLifecycleRenderer* frameLifecycle = toFrameLifecycle(renderer);
if (frameLifecycle != NULL) {
frameLifecycle->beginFrame();
}

for (uint8_t i = 0; i < renderer->maxRows && i < items.size(); i++) {
MenuItem* item = this->items[view + i];
if (item == nullptr) {
Expand Down
19 changes: 19 additions & 0 deletions src/renderer/FrameLifecycleRenderer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once

#include <stdint.h>

/**
* @brief Optional renderer interface for buffered frame lifecycle.
*
* Renderers that require explicit begin/end frame hooks can expose this
* extension through MenuRenderer::queryExtension().
*/
class FrameLifecycleRenderer {
public:
static uint8_t extensionId() { return 1; }

virtual ~FrameLifecycleRenderer() {}

virtual void beginFrame() = 0;
virtual void endFrame() = 0;
};
24 changes: 24 additions & 0 deletions src/renderer/GraphicalMenuItem.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#pragma once

#include <stdint.h>

class GraphicalDisplayInterface;

/**
* @brief Optional capabilities for items rendered on graphical displays.
*/
class GraphicalMenuItem {
public:
static uint8_t capabilityId() { return 1; }

virtual ~GraphicalMenuItem() {}

virtual uint8_t measureGraphicalValueWidth(GraphicalDisplayInterface* display) const {
(void)display;
return 0;
}

virtual bool hasGraphicalToggle() const { return false; }

virtual bool graphicalToggleState() const { return false; }
};
24 changes: 24 additions & 0 deletions src/renderer/GraphicalRendererContext.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#pragma once

#include <stdint.h>

class MenuItem;
class GraphicalDisplayInterface;

/**
* @brief Optional context API exposed by graphical renderers.
*
* This extension allows MenuScreen to share viewport and active-item context
* with renderer implementations that need richer layout information.
*/
class GraphicalRendererContext {
public:
static uint8_t extensionId() { return 2; }

virtual ~GraphicalRendererContext() {}

virtual void setViewportContext(uint8_t viewStart, uint8_t totalItems) = 0;
virtual void setValueAreaWidth(uint8_t width) = 0;
virtual void setActiveItem(const MenuItem* item) = 0;
virtual GraphicalDisplayInterface* getGraphicalDisplay() = 0;
};
22 changes: 20 additions & 2 deletions src/renderer/MenuRenderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,31 @@ class MenuRenderer {
* @brief Gets the maximum number of rows in the display.
* @return Maximum number of rows.
*/
uint8_t getMaxRows() const;
virtual uint8_t getMaxRows() const;

/**
* @brief Gets the maximum number of columns in the display.
* @return Maximum number of columns.
*/
uint8_t getMaxCols() const;
virtual uint8_t getMaxCols() const;

/**
* @brief Optional extension lookup for renderer-specific capabilities.
*
* Returns NULL when extension is not supported.
*/
virtual void* queryExtension(uint8_t extensionId) {
(void)extensionId;
return NULL;
}

/**
* @brief Const overload for optional extension lookup.
*/
virtual const void* queryExtension(uint8_t extensionId) const {
(void)extensionId;
return NULL;
}

/**
* @brief Calculates the available horizontal space for displaying content.
Expand Down
42 changes: 41 additions & 1 deletion test/LcdMenu.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <ItemToggle.h>
#include <MenuItem.h>
#include <display/DisplayInterface.h>
#include <renderer/FrameLifecycleRenderer.h>
#include <renderer/MenuRenderer.h>

#define LCD_ROWS 2
Expand Down Expand Up @@ -59,17 +60,34 @@ class TrackingDisplay : public DisplayInterface {
void setBacklight(bool) override {}
};

class TrackingRenderer : public MenuRenderer {
class TrackingRenderer : public MenuRenderer, public FrameLifecycleRenderer {
public:
TrackingDisplay display;
bool itemDrawn = false;
uint8_t endFrameCalls = 0;
TrackingRenderer() : MenuRenderer(&display, LCD_COLS, LCD_ROWS) {}

void draw(uint8_t) override {}
void drawItem(const char*, const char*, bool) override { itemDrawn = true; }
void clearBlinker() override {}
void drawBlinker() override {}
uint8_t getEffectiveCols() const override { return maxCols; }
void beginFrame() override {}
void endFrame() override { endFrameCalls++; }

void* queryExtension(uint8_t extensionId) override {
if (extensionId == FrameLifecycleRenderer::extensionId()) {
return static_cast<FrameLifecycleRenderer*>(this);
}
return MenuRenderer::queryExtension(extensionId);
}

const void* queryExtension(uint8_t extensionId) const override {
if (extensionId == FrameLifecycleRenderer::extensionId()) {
return static_cast<const FrameLifecycleRenderer*>(this);
}
return MenuRenderer::queryExtension(extensionId);
}
};

// clang-format off
Expand Down Expand Up @@ -143,6 +161,28 @@ unittest(show_enables_and_draws_active_screen) {
assertTrue(renderer.itemDrawn);
}

unittest(refresh_flushes_renderer_frame) {
TrackingRenderer renderer;
LcdMenu menu(renderer);
menu.setScreen(mainScreen);

renderer.endFrameCalls = 0;
menu.refresh();

assertEqual((uint8_t)1, renderer.endFrameCalls);
}

unittest(process_flushes_renderer_when_command_handled) {
TrackingRenderer renderer;
LcdMenu menu(renderer);
menu.setScreen(mainScreen);
menu.setCursor(ITEM_TOGGLE_INDEX);

renderer.endFrameCalls = 0;
assertTrue(menu.process(ENTER));
assertEqual((uint8_t)1, renderer.endFrameCalls);
}

unittest(set_screen_skips_initial_label) {
MenuItem* label = ITEM_LABEL("Title");
MenuItem* item = ITEM_BASIC("Run");
Expand Down
Loading