mirror of
https://github.com/esphome/esphome.git
synced 2024-11-22 06:58:11 +01:00
Add graphical display menu (#4105)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Alex Hermann <gaaf@gmx.net>
This commit is contained in:
parent
86e6a8a503
commit
b30430b0bd
12 changed files with 598 additions and 9 deletions
|
@ -116,6 +116,7 @@ esphome/components/gp8403/* @jesserockz
|
||||||
esphome/components/gpio/* @esphome/core
|
esphome/components/gpio/* @esphome/core
|
||||||
esphome/components/gps/* @coogle
|
esphome/components/gps/* @coogle
|
||||||
esphome/components/graph/* @synco
|
esphome/components/graph/* @synco
|
||||||
|
esphome/components/graphical_display_menu/* @MrMDavidson
|
||||||
esphome/components/gree/* @orestismers
|
esphome/components/gree/* @orestismers
|
||||||
esphome/components/grove_tb6612fng/* @max246
|
esphome/components/grove_tb6612fng/* @max246
|
||||||
esphome/components/growatt_solar/* @leeuwte
|
esphome/components/growatt_solar/* @leeuwte
|
||||||
|
|
|
@ -166,6 +166,13 @@ void Display::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, in
|
||||||
}
|
}
|
||||||
#endif // USE_QR_CODE
|
#endif // USE_QR_CODE
|
||||||
|
|
||||||
|
#ifdef USE_GRAPHICAL_DISPLAY_MENU
|
||||||
|
void Display::menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height) {
|
||||||
|
Rect rect(x, y, width, height);
|
||||||
|
menu->draw(this, &rect);
|
||||||
|
}
|
||||||
|
#endif // USE_GRAPHICAL_DISPLAY_MENU
|
||||||
|
|
||||||
void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1,
|
void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1,
|
||||||
int *width, int *height) {
|
int *width, int *height) {
|
||||||
int x_offset, baseline;
|
int x_offset, baseline;
|
||||||
|
|
|
@ -17,6 +17,10 @@
|
||||||
#include "esphome/components/qr_code/qr_code.h"
|
#include "esphome/components/qr_code/qr_code.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_GRAPHICAL_DISPLAY_MENU
|
||||||
|
#include "esphome/components/graphical_display_menu/graphical_display_menu.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace display {
|
namespace display {
|
||||||
|
|
||||||
|
@ -392,6 +396,17 @@ class Display : public PollingComponent {
|
||||||
void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1);
|
void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_GRAPHICAL_DISPLAY_MENU
|
||||||
|
/**
|
||||||
|
* @param x The x coordinate of the upper left corner
|
||||||
|
* @param y The y coordinate of the upper left corner
|
||||||
|
* @param menu The GraphicalDisplayMenu to draw
|
||||||
|
* @param width Width of the menu
|
||||||
|
* @param height Height of the menu
|
||||||
|
*/
|
||||||
|
void menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height);
|
||||||
|
#endif // USE_GRAPHICAL_DISPLAY_MENU
|
||||||
|
|
||||||
/** Get the text bounds of the given string.
|
/** Get the text bounds of the given string.
|
||||||
*
|
*
|
||||||
* @param x The x coordinate to place the string at, can be 0 if only interested in dimensions.
|
* @param x The x coordinate to place the string at, can be 0 if only interested in dimensions.
|
||||||
|
|
|
@ -172,6 +172,8 @@ void DisplayMenuComponent::show_main() {
|
||||||
|
|
||||||
this->process_initial_();
|
this->process_initial_();
|
||||||
|
|
||||||
|
this->on_before_show();
|
||||||
|
|
||||||
if (this->active_ && this->editing_)
|
if (this->active_ && this->editing_)
|
||||||
this->finish_editing_();
|
this->finish_editing_();
|
||||||
|
|
||||||
|
@ -188,6 +190,8 @@ void DisplayMenuComponent::show_main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
this->draw_and_update();
|
this->draw_and_update();
|
||||||
|
|
||||||
|
this->on_after_show();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisplayMenuComponent::show() {
|
void DisplayMenuComponent::show() {
|
||||||
|
@ -196,18 +200,26 @@ void DisplayMenuComponent::show() {
|
||||||
|
|
||||||
this->process_initial_();
|
this->process_initial_();
|
||||||
|
|
||||||
|
this->on_before_show();
|
||||||
|
|
||||||
if (!this->active_) {
|
if (!this->active_) {
|
||||||
this->active_ = true;
|
this->active_ = true;
|
||||||
this->draw_and_update();
|
this->draw_and_update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this->on_after_show();
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisplayMenuComponent::hide() {
|
void DisplayMenuComponent::hide() {
|
||||||
if (this->check_healthy_and_active_()) {
|
if (this->check_healthy_and_active_()) {
|
||||||
|
this->on_before_hide();
|
||||||
|
|
||||||
if (this->editing_)
|
if (this->editing_)
|
||||||
this->finish_editing_();
|
this->finish_editing_();
|
||||||
this->active_ = false;
|
this->active_ = false;
|
||||||
this->update();
|
this->update();
|
||||||
|
|
||||||
|
this->on_after_hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,11 @@ class DisplayMenuComponent : public Component {
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
virtual void on_before_show(){};
|
||||||
|
virtual void on_after_show(){};
|
||||||
|
virtual void on_before_hide(){};
|
||||||
|
virtual void on_after_hide(){};
|
||||||
|
|
||||||
uint8_t rows_;
|
uint8_t rows_;
|
||||||
bool active_;
|
bool active_;
|
||||||
MenuMode mode_;
|
MenuMode mode_;
|
||||||
|
|
|
@ -5,6 +5,29 @@
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace display_menu_base {
|
namespace display_menu_base {
|
||||||
|
|
||||||
|
const LogString *menu_item_type_to_string(MenuItemType type) {
|
||||||
|
switch (type) {
|
||||||
|
case MenuItemType::MENU_ITEM_LABEL:
|
||||||
|
return LOG_STR("MENU_ITEM_LABEL");
|
||||||
|
case MenuItemType::MENU_ITEM_MENU:
|
||||||
|
return LOG_STR("MENU_ITEM_MENU");
|
||||||
|
case MenuItemType::MENU_ITEM_BACK:
|
||||||
|
return LOG_STR("MENU_ITEM_BACK");
|
||||||
|
case MenuItemType::MENU_ITEM_SELECT:
|
||||||
|
return LOG_STR("MENU_ITEM_SELECT");
|
||||||
|
case MenuItemType::MENU_ITEM_NUMBER:
|
||||||
|
return LOG_STR("MENU_ITEM_NUMBER");
|
||||||
|
case MenuItemType::MENU_ITEM_SWITCH:
|
||||||
|
return LOG_STR("MENU_ITEM_SWITCH");
|
||||||
|
case MenuItemType::MENU_ITEM_COMMAND:
|
||||||
|
return LOG_STR("MENU_ITEM_COMMAND");
|
||||||
|
case MenuItemType::MENU_ITEM_CUSTOM:
|
||||||
|
return LOG_STR("MENU_ITEM_CUSTOM");
|
||||||
|
default:
|
||||||
|
return LOG_STR("UNKNOWN");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MenuItem::on_enter() { this->on_enter_callbacks_.call(); }
|
void MenuItem::on_enter() { this->on_enter_callbacks_.call(); }
|
||||||
|
|
||||||
void MenuItem::on_leave() { this->on_leave_callbacks_.call(); }
|
void MenuItem::on_leave() { this->on_leave_callbacks_.call(); }
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace display_menu_base {
|
namespace display_menu_base {
|
||||||
|
@ -29,6 +30,9 @@ enum MenuItemType {
|
||||||
MENU_ITEM_CUSTOM,
|
MENU_ITEM_CUSTOM,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// @brief Returns a string representation of a menu item type suitable for logging
|
||||||
|
const LogString *menu_item_type_to_string(MenuItemType type);
|
||||||
|
|
||||||
class MenuItem;
|
class MenuItem;
|
||||||
class MenuItemMenu;
|
class MenuItemMenu;
|
||||||
using value_getter_t = std::function<std::string(const MenuItem *)>;
|
using value_getter_t = std::function<std::string(const MenuItem *)>;
|
||||||
|
|
96
esphome/components/graphical_display_menu/__init__.py
Normal file
96
esphome/components/graphical_display_menu/__init__.py
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.components import display, font, color
|
||||||
|
from esphome.const import CONF_ID, CONF_TRIGGER_ID
|
||||||
|
from esphome import automation, core
|
||||||
|
|
||||||
|
from esphome.components.display_menu_base import (
|
||||||
|
DISPLAY_MENU_BASE_SCHEMA,
|
||||||
|
DisplayMenuComponent,
|
||||||
|
display_menu_to_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
CONF_DISPLAY = "display"
|
||||||
|
CONF_FONT = "font"
|
||||||
|
CONF_MENU_ITEM_VALUE = "menu_item_value"
|
||||||
|
CONF_FOREGROUND_COLOR = "foreground_color"
|
||||||
|
CONF_BACKGROUND_COLOR = "background_color"
|
||||||
|
CONF_ON_REDRAW = "on_redraw"
|
||||||
|
|
||||||
|
graphical_display_menu_ns = cg.esphome_ns.namespace("graphical_display_menu")
|
||||||
|
GraphicalDisplayMenu = graphical_display_menu_ns.class_(
|
||||||
|
"GraphicalDisplayMenu", DisplayMenuComponent
|
||||||
|
)
|
||||||
|
GraphicalDisplayMenuConstPtr = GraphicalDisplayMenu.operator("ptr").operator("const")
|
||||||
|
MenuItemValueArguments = graphical_display_menu_ns.struct("MenuItemValueArguments")
|
||||||
|
MenuItemValueArgumentsConstPtr = MenuItemValueArguments.operator("ptr").operator(
|
||||||
|
"const"
|
||||||
|
)
|
||||||
|
GraphicalDisplayMenuOnRedrawTrigger = graphical_display_menu_ns.class_(
|
||||||
|
"GraphicalDisplayMenuOnRedrawTrigger", automation.Trigger
|
||||||
|
)
|
||||||
|
|
||||||
|
CODEOWNERS = ["@MrMDavidson"]
|
||||||
|
|
||||||
|
AUTO_LOAD = ["display_menu_base"]
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = DISPLAY_MENU_BASE_SCHEMA.extend(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(GraphicalDisplayMenu),
|
||||||
|
cv.Optional(CONF_DISPLAY): cv.use_id(display.DisplayBuffer),
|
||||||
|
cv.Required(CONF_FONT): cv.use_id(font.Font),
|
||||||
|
cv.Optional(CONF_MENU_ITEM_VALUE): cv.templatable(cv.string),
|
||||||
|
cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct),
|
||||||
|
cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct),
|
||||||
|
cv.Optional(CONF_ON_REDRAW): automation.validate_automation(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||||
|
GraphicalDisplayMenuOnRedrawTrigger
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
|
if display_config := config.get(CONF_DISPLAY):
|
||||||
|
drawing_display = await cg.get_variable(display_config)
|
||||||
|
cg.add(var.set_display(drawing_display))
|
||||||
|
|
||||||
|
menu_font = await cg.get_variable(config[CONF_FONT])
|
||||||
|
cg.add(var.set_font(menu_font))
|
||||||
|
|
||||||
|
if (menu_item_value_config := config.get(CONF_MENU_ITEM_VALUE, None)) is not None:
|
||||||
|
if isinstance(menu_item_value_config, core.Lambda):
|
||||||
|
template_ = await cg.templatable(
|
||||||
|
menu_item_value_config,
|
||||||
|
[(MenuItemValueArgumentsConstPtr, "it")],
|
||||||
|
cg.std_string,
|
||||||
|
)
|
||||||
|
cg.add(var.set_menu_item_value(template_))
|
||||||
|
else:
|
||||||
|
cg.add(var.set_menu_item_value(menu_item_value_config))
|
||||||
|
|
||||||
|
if foreground_color_config := config.get(CONF_FOREGROUND_COLOR):
|
||||||
|
foreground_color = await cg.get_variable(foreground_color_config)
|
||||||
|
cg.add(var.set_foreground_color(foreground_color))
|
||||||
|
|
||||||
|
if background_color_config := config.get(CONF_BACKGROUND_COLOR):
|
||||||
|
background_color = await cg.get_variable(background_color_config)
|
||||||
|
cg.add(var.set_background_color(background_color))
|
||||||
|
|
||||||
|
for conf in config.get(CONF_ON_REDRAW, []):
|
||||||
|
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||||
|
await automation.build_automation(
|
||||||
|
trigger, [(GraphicalDisplayMenuConstPtr, "it")], conf
|
||||||
|
)
|
||||||
|
|
||||||
|
await display_menu_to_code(var, config)
|
||||||
|
|
||||||
|
cg.add_define("USE_GRAPHICAL_DISPLAY_MENU")
|
|
@ -0,0 +1,243 @@
|
||||||
|
#include "graphical_display_menu.h"
|
||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include <cstdlib>
|
||||||
|
#include "esphome/components/display/display.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace graphical_display_menu {
|
||||||
|
|
||||||
|
static const char *const TAG = "graphical_display_menu";
|
||||||
|
|
||||||
|
void GraphicalDisplayMenu::setup() {
|
||||||
|
if (this->display_ != nullptr) {
|
||||||
|
display::display_writer_t writer = [this](display::Display &it) { this->draw_menu(); };
|
||||||
|
this->display_page_ = make_unique<display::DisplayPage>(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this->menu_item_value_.has_value()) {
|
||||||
|
this->menu_item_value_ = [](const MenuItemValueArguments *it) {
|
||||||
|
std::string label = " ";
|
||||||
|
if (it->is_item_selected && it->is_menu_editing) {
|
||||||
|
label.append(">");
|
||||||
|
label.append(it->item->get_value_text());
|
||||||
|
label.append("<");
|
||||||
|
} else {
|
||||||
|
label.append("(");
|
||||||
|
label.append(it->item->get_value_text());
|
||||||
|
label.append(")");
|
||||||
|
}
|
||||||
|
return label;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
display_menu_base::DisplayMenuComponent::setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphicalDisplayMenu::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "Graphical Display Menu");
|
||||||
|
ESP_LOGCONFIG(TAG, "Has Display: %s", YESNO(this->display_ != nullptr));
|
||||||
|
ESP_LOGCONFIG(TAG, "Popup Mode: %s", YESNO(this->display_ != nullptr));
|
||||||
|
ESP_LOGCONFIG(TAG, "Advanced Drawing Mode: %s", YESNO(this->display_ == nullptr));
|
||||||
|
ESP_LOGCONFIG(TAG, "Has Font: %s", YESNO(this->font_ != nullptr));
|
||||||
|
ESP_LOGCONFIG(TAG, "Mode: %s", this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick");
|
||||||
|
ESP_LOGCONFIG(TAG, "Active: %s", YESNO(this->active_));
|
||||||
|
ESP_LOGCONFIG(TAG, "Menu items:");
|
||||||
|
for (size_t i = 0; i < this->displayed_item_->items_size(); i++) {
|
||||||
|
auto *item = this->displayed_item_->get_item(i);
|
||||||
|
ESP_LOGCONFIG(TAG, " %i: %s (Type: %s, Immediate Edit: %s)", i, item->get_text().c_str(),
|
||||||
|
LOG_STR_ARG(display_menu_base::menu_item_type_to_string(item->get_type())),
|
||||||
|
YESNO(item->get_immediate_edit()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphicalDisplayMenu::set_display(display::Display *display) { this->display_ = display; }
|
||||||
|
|
||||||
|
void GraphicalDisplayMenu::set_font(display::BaseFont *font) { this->font_ = font; }
|
||||||
|
|
||||||
|
void GraphicalDisplayMenu::set_foreground_color(Color foreground_color) { this->foreground_color_ = foreground_color; }
|
||||||
|
void GraphicalDisplayMenu::set_background_color(Color background_color) { this->background_color_ = background_color; }
|
||||||
|
|
||||||
|
void GraphicalDisplayMenu::on_before_show() {
|
||||||
|
if (this->display_ != nullptr) {
|
||||||
|
this->previous_display_page_ = this->display_->get_active_page();
|
||||||
|
this->display_->show_page(this->display_page_.get());
|
||||||
|
this->display_->clear();
|
||||||
|
} else {
|
||||||
|
this->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphicalDisplayMenu::on_before_hide() {
|
||||||
|
if (this->previous_display_page_ != nullptr) {
|
||||||
|
this->display_->show_page((display::DisplayPage *) this->previous_display_page_);
|
||||||
|
this->display_->clear();
|
||||||
|
this->update();
|
||||||
|
this->previous_display_page_ = nullptr;
|
||||||
|
} else {
|
||||||
|
this->update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphicalDisplayMenu::draw_and_update() {
|
||||||
|
this->update();
|
||||||
|
|
||||||
|
// If we're in advanced drawing mode we won't have a display and will instead require the update callback to do
|
||||||
|
// our drawing
|
||||||
|
if (this->display_ != nullptr) {
|
||||||
|
draw_menu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphicalDisplayMenu::draw_menu() {
|
||||||
|
if (this->display_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "draw_menu() called without a display_. This is only available when using the menu in pop up mode");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
display::Rect bounds(0, 0, this->display_->get_width(), this->display_->get_height());
|
||||||
|
this->draw_menu_internal_(this->display_, &bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphicalDisplayMenu::draw(display::Display *display, const display::Rect *bounds) {
|
||||||
|
this->draw_menu_internal_(display, bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const display::Rect *bounds) {
|
||||||
|
int total_height = 0;
|
||||||
|
int y_padding = 2;
|
||||||
|
bool scroll_menu_items = false;
|
||||||
|
std::vector<display::Rect> menu_dimensions;
|
||||||
|
int number_items_fit_to_screen = 0;
|
||||||
|
const int max_item_index = this->displayed_item_->items_size() - 1;
|
||||||
|
|
||||||
|
for (size_t i = 0; i <= max_item_index; i++) {
|
||||||
|
const auto *item = this->displayed_item_->get_item(i);
|
||||||
|
const bool selected = i == this->cursor_index_;
|
||||||
|
const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected);
|
||||||
|
|
||||||
|
menu_dimensions.push_back(item_dimensions);
|
||||||
|
total_height += item_dimensions.h + (i == 0 ? 0 : y_padding);
|
||||||
|
|
||||||
|
if (total_height <= bounds->h) {
|
||||||
|
number_items_fit_to_screen++;
|
||||||
|
} else {
|
||||||
|
// Scroll the display if the selected item or the item immediately after it overflows
|
||||||
|
if ((selected) || (i == this->cursor_index_ + 1)) {
|
||||||
|
scroll_menu_items = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine what items to draw
|
||||||
|
int first_item_index = 0;
|
||||||
|
int last_item_index = max_item_index;
|
||||||
|
|
||||||
|
if (number_items_fit_to_screen <= 1) {
|
||||||
|
// If only one item can fit to the bounds draw the current cursor item
|
||||||
|
last_item_index = std::min(last_item_index, this->cursor_index_ + 1);
|
||||||
|
first_item_index = this->cursor_index_;
|
||||||
|
} else {
|
||||||
|
if (scroll_menu_items) {
|
||||||
|
// Attempt to draw the item after the current item (+1 for equality check in the draw loop)
|
||||||
|
last_item_index = std::min(last_item_index, this->cursor_index_ + 1);
|
||||||
|
|
||||||
|
// Go back through the measurements to determine how many prior items we can fit
|
||||||
|
int height_left_to_use = bounds->h;
|
||||||
|
for (int i = last_item_index; i >= 0; i--) {
|
||||||
|
const display::Rect item_dimensions = menu_dimensions[i];
|
||||||
|
height_left_to_use -= (item_dimensions.h + y_padding);
|
||||||
|
|
||||||
|
if (height_left_to_use <= 0) {
|
||||||
|
// Ran out of space - this is our first item to draw
|
||||||
|
first_item_index = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const int items_to_draw = last_item_index - first_item_index;
|
||||||
|
// Dont't draw last item partially if it is the selected item
|
||||||
|
if ((this->cursor_index_ == last_item_index) && (number_items_fit_to_screen <= items_to_draw) &&
|
||||||
|
(first_item_index < max_item_index)) {
|
||||||
|
first_item_index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the items into the view port
|
||||||
|
display->start_clipping(*bounds);
|
||||||
|
|
||||||
|
int y_offset = bounds->y;
|
||||||
|
for (size_t i = first_item_index; i <= last_item_index; i++) {
|
||||||
|
const auto *item = this->displayed_item_->get_item(i);
|
||||||
|
const bool selected = i == this->cursor_index_;
|
||||||
|
display::Rect dimensions = menu_dimensions[i];
|
||||||
|
|
||||||
|
dimensions.y = y_offset;
|
||||||
|
dimensions.x = bounds->x;
|
||||||
|
this->draw_item(display, item, &dimensions, selected);
|
||||||
|
|
||||||
|
y_offset = dimensions.y + dimensions.h + y_padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
display->end_clipping();
|
||||||
|
}
|
||||||
|
|
||||||
|
display::Rect GraphicalDisplayMenu::measure_item(display::Display *display, const display_menu_base::MenuItem *item,
|
||||||
|
const display::Rect *bounds, const bool selected) {
|
||||||
|
display::Rect dimensions(0, 0, 0, 0);
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
// TODO: Support selection glyph
|
||||||
|
dimensions.w += 0;
|
||||||
|
dimensions.h += 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string label = item->get_text();
|
||||||
|
if (item->has_value()) {
|
||||||
|
// Append to label
|
||||||
|
MenuItemValueArguments args(item, selected, this->editing_);
|
||||||
|
label.append(this->menu_item_value_.value(&args));
|
||||||
|
}
|
||||||
|
|
||||||
|
int x1;
|
||||||
|
int y1;
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
display->get_text_bounds(0, 0, label.c_str(), this->font_, display::TextAlign::TOP_LEFT, &x1, &y1, &width, &height);
|
||||||
|
|
||||||
|
dimensions.w = std::min((int16_t) width, bounds->w);
|
||||||
|
dimensions.h = std::min((int16_t) height, bounds->h);
|
||||||
|
|
||||||
|
return dimensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void GraphicalDisplayMenu::draw_item(display::Display *display, const display_menu_base::MenuItem *item,
|
||||||
|
const display::Rect *bounds, const bool selected) {
|
||||||
|
const auto background_color = selected ? this->foreground_color_ : this->background_color_;
|
||||||
|
const auto foreground_color = selected ? this->background_color_ : this->foreground_color_;
|
||||||
|
|
||||||
|
// int background_width = std::max(bounds->width, available_width);
|
||||||
|
int background_width = bounds->w;
|
||||||
|
|
||||||
|
if (selected) {
|
||||||
|
display->filled_rectangle(bounds->x, bounds->y, background_width, bounds->h, background_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string label = item->get_text();
|
||||||
|
if (item->has_value()) {
|
||||||
|
MenuItemValueArguments args(item, selected, this->editing_);
|
||||||
|
label.append(this->menu_item_value_.value(&args));
|
||||||
|
}
|
||||||
|
|
||||||
|
display->print(bounds->x, bounds->y, this->font_, foreground_color, display::TextAlign::TOP_LEFT, label.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, const uint8_t row, const bool selected) {
|
||||||
|
ESP_LOGE(TAG, "draw_item(MenuItem *item, uint8_t row, bool selected) called. The graphical_display_menu specific "
|
||||||
|
"draw_item should be called.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void GraphicalDisplayMenu::update() { this->on_redraw_callbacks_.call(); }
|
||||||
|
|
||||||
|
} // namespace graphical_display_menu
|
||||||
|
} // namespace esphome
|
|
@ -0,0 +1,84 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/color.h"
|
||||||
|
#include "esphome/components/display_menu_base/display_menu_base.h"
|
||||||
|
#include "esphome/components/display_menu_base/menu_item.h"
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
|
||||||
|
// forward declare from display namespace
|
||||||
|
namespace display {
|
||||||
|
class Display;
|
||||||
|
class DisplayPage;
|
||||||
|
class BaseFont;
|
||||||
|
class Rect;
|
||||||
|
} // namespace display
|
||||||
|
|
||||||
|
namespace graphical_display_menu {
|
||||||
|
|
||||||
|
const Color COLOR_ON(255, 255, 255, 255);
|
||||||
|
const Color COLOR_OFF(0, 0, 0, 0);
|
||||||
|
|
||||||
|
struct MenuItemValueArguments {
|
||||||
|
MenuItemValueArguments(const display_menu_base::MenuItem *item, bool is_item_selected, bool is_menu_editing) {
|
||||||
|
this->item = item;
|
||||||
|
this->is_item_selected = is_item_selected;
|
||||||
|
this->is_menu_editing = is_menu_editing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const display_menu_base::MenuItem *item;
|
||||||
|
bool is_item_selected;
|
||||||
|
bool is_menu_editing;
|
||||||
|
};
|
||||||
|
|
||||||
|
class GraphicalDisplayMenu : public display_menu_base::DisplayMenuComponent {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
void set_display(display::Display *display);
|
||||||
|
void set_font(display::BaseFont *font);
|
||||||
|
template<typename V> void set_menu_item_value(V menu_item_value) { this->menu_item_value_ = menu_item_value; }
|
||||||
|
void set_foreground_color(Color foreground_color);
|
||||||
|
void set_background_color(Color background_color);
|
||||||
|
|
||||||
|
void add_on_redraw_callback(std::function<void()> &&cb) { this->on_redraw_callbacks_.add(std::move(cb)); }
|
||||||
|
|
||||||
|
void draw(display::Display *display, const display::Rect *bounds);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void draw_and_update() override;
|
||||||
|
void draw_menu() override;
|
||||||
|
void draw_menu_internal_(display::Display *display, const display::Rect *bounds);
|
||||||
|
void draw_item(const display_menu_base::MenuItem *item, uint8_t row, bool selected) override;
|
||||||
|
virtual display::Rect measure_item(display::Display *display, const display_menu_base::MenuItem *item,
|
||||||
|
const display::Rect *bounds, bool selected);
|
||||||
|
virtual void draw_item(display::Display *display, const display_menu_base::MenuItem *item,
|
||||||
|
const display::Rect *bounds, bool selected);
|
||||||
|
void update() override;
|
||||||
|
|
||||||
|
void on_before_show() override;
|
||||||
|
void on_before_hide() override;
|
||||||
|
|
||||||
|
std::unique_ptr<display::DisplayPage> display_page_{nullptr};
|
||||||
|
const display::DisplayPage *previous_display_page_{nullptr};
|
||||||
|
display::Display *display_{nullptr};
|
||||||
|
display::BaseFont *font_{nullptr};
|
||||||
|
TemplatableValue<std::string, const MenuItemValueArguments *> menu_item_value_;
|
||||||
|
Color foreground_color_{COLOR_ON};
|
||||||
|
Color background_color_{COLOR_OFF};
|
||||||
|
|
||||||
|
CallbackManager<void()> on_redraw_callbacks_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
class GraphicalDisplayMenuOnRedrawTrigger : public Trigger<const GraphicalDisplayMenu *> {
|
||||||
|
public:
|
||||||
|
explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) {
|
||||||
|
parent->add_on_redraw_callback([this, parent]() { this->trigger(parent); });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace graphical_display_menu
|
||||||
|
} // namespace esphome
|
|
@ -51,6 +51,7 @@
|
||||||
#define USE_UART_DEBUGGER
|
#define USE_UART_DEBUGGER
|
||||||
#define USE_WIFI
|
#define USE_WIFI
|
||||||
#define USE_WIFI_AP
|
#define USE_WIFI_AP
|
||||||
|
#define USE_GRAPHICAL_DISPLAY_MENU
|
||||||
|
|
||||||
// Arduino-specific feature flags
|
// Arduino-specific feature flags
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
|
|
116
tests/test1.yaml
116
tests/test1.yaml
|
@ -1141,10 +1141,12 @@ sensor:
|
||||||
value: !lambda "return -1;"
|
value: !lambda "return -1;"
|
||||||
on_clockwise:
|
on_clockwise:
|
||||||
- logger.log: Clockwise
|
- logger.log: Clockwise
|
||||||
- display_menu.down:
|
- display_menu.down: test_lcd_menu
|
||||||
|
- display_menu.down: test_graphical_display_menu
|
||||||
on_anticlockwise:
|
on_anticlockwise:
|
||||||
- logger.log: Anticlockwise
|
- logger.log: Anticlockwise
|
||||||
- display_menu.up:
|
- display_menu.up: test_lcd_menu
|
||||||
|
- display_menu.up: test_graphical_display_menu
|
||||||
- platform: pulse_width
|
- platform: pulse_width
|
||||||
name: Pulse Width
|
name: Pulse Width
|
||||||
pin:
|
pin:
|
||||||
|
@ -1781,13 +1783,22 @@ binary_sensor:
|
||||||
on_press:
|
on_press:
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
display_menu.is_active:
|
display_menu.is_active: test_lcd_menu
|
||||||
then:
|
then:
|
||||||
- display_menu.enter:
|
- display_menu.enter: test_lcd_menu
|
||||||
else:
|
else:
|
||||||
- display_menu.left:
|
- display_menu.left: test_lcd_menu
|
||||||
- display_menu.right:
|
- display_menu.right: test_lcd_menu
|
||||||
- display_menu.show:
|
- display_menu.show: test_lcd_menu
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
display_menu.is_active: test_graphical_display_menu
|
||||||
|
then:
|
||||||
|
- display_menu.enter: test_graphical_display_menu
|
||||||
|
else:
|
||||||
|
- display_menu.left: test_graphical_display_menu
|
||||||
|
- display_menu.right: test_graphical_display_menu
|
||||||
|
- display_menu.show: test_graphical_display_menu
|
||||||
- platform: template
|
- platform: template
|
||||||
name: Garage Door Open
|
name: Garage Door Open
|
||||||
id: garage_door
|
id: garage_door
|
||||||
|
@ -3204,6 +3215,7 @@ display:
|
||||||
lambda: |-
|
lambda: |-
|
||||||
it.rectangle(0, 0, it.get_width(), it.get_height());
|
it.rectangle(0, 0, it.get_width(), it.get_height());
|
||||||
- platform: st7735
|
- platform: st7735
|
||||||
|
id: st7735_display
|
||||||
model: INITR_BLACKTAB
|
model: INITR_BLACKTAB
|
||||||
cs_pin:
|
cs_pin:
|
||||||
allow_other_uses: true
|
allow_other_uses: true
|
||||||
|
@ -3997,6 +4009,7 @@ ld2420:
|
||||||
uart_id: ld2420_uart
|
uart_id: ld2420_uart
|
||||||
|
|
||||||
lcd_menu:
|
lcd_menu:
|
||||||
|
id: test_lcd_menu
|
||||||
display_id: my_lcd_gpio
|
display_id: my_lcd_gpio
|
||||||
mark_back: 0x5e
|
mark_back: 0x5e
|
||||||
mark_selected: 0x3e
|
mark_selected: 0x3e
|
||||||
|
@ -4028,7 +4041,7 @@ lcd_menu:
|
||||||
text: Show Main
|
text: Show Main
|
||||||
on_value:
|
on_value:
|
||||||
then:
|
then:
|
||||||
- display_menu.show_main:
|
- display_menu.show_main: test_lcd_menu
|
||||||
- type: select
|
- type: select
|
||||||
text: Enum Item
|
text: Enum Item
|
||||||
immediate_edit: true
|
immediate_edit: true
|
||||||
|
@ -4058,7 +4071,7 @@ lcd_menu:
|
||||||
text: Hide
|
text: Hide
|
||||||
on_value:
|
on_value:
|
||||||
then:
|
then:
|
||||||
- display_menu.hide:
|
- display_menu.hide: test_lcd_menu
|
||||||
- type: switch
|
- type: switch
|
||||||
text: Switch
|
text: Switch
|
||||||
switch: my_switch
|
switch: my_switch
|
||||||
|
@ -4078,6 +4091,91 @@ lcd_menu:
|
||||||
then:
|
then:
|
||||||
lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());'
|
lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());'
|
||||||
|
|
||||||
|
font:
|
||||||
|
- file: "gfonts://Roboto"
|
||||||
|
id: roboto
|
||||||
|
size: 20
|
||||||
|
|
||||||
|
graphical_display_menu:
|
||||||
|
id: test_graphical_display_menu
|
||||||
|
display: st7735_display
|
||||||
|
font: roboto
|
||||||
|
active: false
|
||||||
|
mode: rotary
|
||||||
|
on_enter:
|
||||||
|
then:
|
||||||
|
lambda: 'ESP_LOGI("graphical_display_menu", "root enter");'
|
||||||
|
on_leave:
|
||||||
|
then:
|
||||||
|
lambda: 'ESP_LOGI("graphical_display_menu", "root leave");'
|
||||||
|
items:
|
||||||
|
- type: back
|
||||||
|
text: 'Back'
|
||||||
|
- type: label
|
||||||
|
- type: menu
|
||||||
|
text: 'Submenu 1'
|
||||||
|
items:
|
||||||
|
- type: back
|
||||||
|
text: 'Back'
|
||||||
|
- type: menu
|
||||||
|
text: 'Submenu 21'
|
||||||
|
items:
|
||||||
|
- type: back
|
||||||
|
text: 'Back'
|
||||||
|
- type: command
|
||||||
|
text: 'Show Main'
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
- display_menu.show_main: test_graphical_display_menu
|
||||||
|
- type: select
|
||||||
|
text: 'Enum Item'
|
||||||
|
immediate_edit: true
|
||||||
|
select: test_select
|
||||||
|
on_enter:
|
||||||
|
then:
|
||||||
|
lambda: 'ESP_LOGI("graphical_display_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
|
||||||
|
on_leave:
|
||||||
|
then:
|
||||||
|
lambda: 'ESP_LOGI("graphical_display_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
lambda: 'ESP_LOGI("graphical_display_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
|
||||||
|
- type: number
|
||||||
|
text: 'Number'
|
||||||
|
number: test_number
|
||||||
|
on_enter:
|
||||||
|
then:
|
||||||
|
lambda: 'ESP_LOGI("graphical_display_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
|
||||||
|
on_leave:
|
||||||
|
then:
|
||||||
|
lambda: 'ESP_LOGI("graphical_display_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
lambda: 'ESP_LOGI("graphical_display_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
|
||||||
|
- type: command
|
||||||
|
text: 'Hide'
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
- display_menu.hide: test_graphical_display_menu
|
||||||
|
- type: switch
|
||||||
|
text: 'Switch'
|
||||||
|
switch: my_switch
|
||||||
|
on_text: 'Bright'
|
||||||
|
off_text: 'Dark'
|
||||||
|
immediate_edit: false
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
lambda: 'ESP_LOGI("graphical_display_menu", "switch value: %s", it->get_value_text().c_str());'
|
||||||
|
- type: custom
|
||||||
|
text: !lambda 'return "Custom";'
|
||||||
|
value_lambda: 'return "Val";'
|
||||||
|
on_next:
|
||||||
|
then:
|
||||||
|
lambda: 'ESP_LOGI("graphical_display_menu", "custom next: %s", it->get_text().c_str());'
|
||||||
|
on_prev:
|
||||||
|
then:
|
||||||
|
lambda: 'ESP_LOGI("graphical_display_menu", "custom prev: %s", it->get_text().c_str());'
|
||||||
|
|
||||||
alarm_control_panel:
|
alarm_control_panel:
|
||||||
- platform: template
|
- platform: template
|
||||||
id: alarmcontrolpanel1
|
id: alarmcontrolpanel1
|
||||||
|
|
Loading…
Reference in a new issue