From b30430b0bda4f12e72281c987183d7c7a45cc659 Mon Sep 17 00:00:00 2001 From: Michael Davidson Date: Tue, 12 Dec 2023 14:15:59 +1100 Subject: [PATCH] Add graphical display menu (#4105) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Alex Hermann --- CODEOWNERS | 1 + esphome/components/display/display.cpp | 7 + esphome/components/display/display.h | 15 ++ .../display_menu_base/display_menu_base.cpp | 12 + .../display_menu_base/display_menu_base.h | 5 + .../display_menu_base/menu_item.cpp | 23 ++ .../components/display_menu_base/menu_item.h | 4 + .../graphical_display_menu/__init__.py | 96 +++++++ .../graphical_display_menu.cpp | 243 ++++++++++++++++++ .../graphical_display_menu.h | 84 ++++++ esphome/core/defines.h | 1 + tests/test1.yaml | 116 ++++++++- 12 files changed, 598 insertions(+), 9 deletions(-) create mode 100644 esphome/components/graphical_display_menu/__init__.py create mode 100644 esphome/components/graphical_display_menu/graphical_display_menu.cpp create mode 100644 esphome/components/graphical_display_menu/graphical_display_menu.h diff --git a/CODEOWNERS b/CODEOWNERS index de80806eac..43b7647fe6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -116,6 +116,7 @@ esphome/components/gp8403/* @jesserockz esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle esphome/components/graph/* @synco +esphome/components/graphical_display_menu/* @MrMDavidson esphome/components/gree/* @orestismers esphome/components/grove_tb6612fng/* @max246 esphome/components/growatt_solar/* @leeuwte diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 22454aeddb..88ee64ea55 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -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 +#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, int *width, int *height) { int x_offset, baseline; diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index 7ce6d179ef..3afcfb9528 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -17,6 +17,10 @@ #include "esphome/components/qr_code/qr_code.h" #endif +#ifdef USE_GRAPHICAL_DISPLAY_MENU +#include "esphome/components/graphical_display_menu/graphical_display_menu.h" +#endif + namespace esphome { 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); #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. * * @param x The x coordinate to place the string at, can be 0 if only interested in dimensions. diff --git a/esphome/components/display_menu_base/display_menu_base.cpp b/esphome/components/display_menu_base/display_menu_base.cpp index 57da3cec35..0bfee338ca 100644 --- a/esphome/components/display_menu_base/display_menu_base.cpp +++ b/esphome/components/display_menu_base/display_menu_base.cpp @@ -172,6 +172,8 @@ void DisplayMenuComponent::show_main() { this->process_initial_(); + this->on_before_show(); + if (this->active_ && this->editing_) this->finish_editing_(); @@ -188,6 +190,8 @@ void DisplayMenuComponent::show_main() { } this->draw_and_update(); + + this->on_after_show(); } void DisplayMenuComponent::show() { @@ -196,18 +200,26 @@ void DisplayMenuComponent::show() { this->process_initial_(); + this->on_before_show(); + if (!this->active_) { this->active_ = true; this->draw_and_update(); } + + this->on_after_show(); } void DisplayMenuComponent::hide() { if (this->check_healthy_and_active_()) { + this->on_before_hide(); + if (this->editing_) this->finish_editing_(); this->active_ = false; this->update(); + + this->on_after_hide(); } } diff --git a/esphome/components/display_menu_base/display_menu_base.h b/esphome/components/display_menu_base/display_menu_base.h index 46bb0a8192..6208fcd3b4 100644 --- a/esphome/components/display_menu_base/display_menu_base.h +++ b/esphome/components/display_menu_base/display_menu_base.h @@ -60,6 +60,11 @@ class DisplayMenuComponent : public Component { update(); } + virtual void on_before_show(){}; + virtual void on_after_show(){}; + virtual void on_before_hide(){}; + virtual void on_after_hide(){}; + uint8_t rows_; bool active_; MenuMode mode_; diff --git a/esphome/components/display_menu_base/menu_item.cpp b/esphome/components/display_menu_base/menu_item.cpp index bbe6ec0e89..2c7f34c493 100644 --- a/esphome/components/display_menu_base/menu_item.cpp +++ b/esphome/components/display_menu_base/menu_item.cpp @@ -5,6 +5,29 @@ namespace esphome { 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_leave() { this->on_leave_callbacks_.call(); } diff --git a/esphome/components/display_menu_base/menu_item.h b/esphome/components/display_menu_base/menu_item.h index a30f31e88f..36de146031 100644 --- a/esphome/components/display_menu_base/menu_item.h +++ b/esphome/components/display_menu_base/menu_item.h @@ -14,6 +14,7 @@ #endif #include +#include "esphome/core/log.h" namespace esphome { namespace display_menu_base { @@ -29,6 +30,9 @@ enum MenuItemType { 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 MenuItemMenu; using value_getter_t = std::function; diff --git a/esphome/components/graphical_display_menu/__init__.py b/esphome/components/graphical_display_menu/__init__.py new file mode 100644 index 0000000000..dc49358efd --- /dev/null +++ b/esphome/components/graphical_display_menu/__init__.py @@ -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") diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp new file mode 100644 index 0000000000..2e4c14fb7b --- /dev/null +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -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 +#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(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 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 diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.h b/esphome/components/graphical_display_menu/graphical_display_menu.h new file mode 100644 index 0000000000..96f2bd79fd --- /dev/null +++ b/esphome/components/graphical_display_menu/graphical_display_menu.h @@ -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 + +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 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 &&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_page_{nullptr}; + const display::DisplayPage *previous_display_page_{nullptr}; + display::Display *display_{nullptr}; + display::BaseFont *font_{nullptr}; + TemplatableValue menu_item_value_; + Color foreground_color_{COLOR_ON}; + Color background_color_{COLOR_OFF}; + + CallbackManager on_redraw_callbacks_{}; +}; + +class GraphicalDisplayMenuOnRedrawTrigger : public Trigger { + public: + explicit GraphicalDisplayMenuOnRedrawTrigger(GraphicalDisplayMenu *parent) { + parent->add_on_redraw_callback([this, parent]() { this->trigger(parent); }); + } +}; + +} // namespace graphical_display_menu +} // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index b93b8c9270..e75abdb88f 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -51,6 +51,7 @@ #define USE_UART_DEBUGGER #define USE_WIFI #define USE_WIFI_AP +#define USE_GRAPHICAL_DISPLAY_MENU // Arduino-specific feature flags #ifdef USE_ARDUINO diff --git a/tests/test1.yaml b/tests/test1.yaml index c90cdf6d90..559b7ab8fc 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1141,10 +1141,12 @@ sensor: value: !lambda "return -1;" on_clockwise: - logger.log: Clockwise - - display_menu.down: + - display_menu.down: test_lcd_menu + - display_menu.down: test_graphical_display_menu on_anticlockwise: - logger.log: Anticlockwise - - display_menu.up: + - display_menu.up: test_lcd_menu + - display_menu.up: test_graphical_display_menu - platform: pulse_width name: Pulse Width pin: @@ -1781,13 +1783,22 @@ binary_sensor: on_press: - if: condition: - display_menu.is_active: + display_menu.is_active: test_lcd_menu then: - - display_menu.enter: + - display_menu.enter: test_lcd_menu else: - - display_menu.left: - - display_menu.right: - - display_menu.show: + - display_menu.left: test_lcd_menu + - display_menu.right: test_lcd_menu + - 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 name: Garage Door Open id: garage_door @@ -3204,6 +3215,7 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: st7735 + id: st7735_display model: INITR_BLACKTAB cs_pin: allow_other_uses: true @@ -3997,6 +4009,7 @@ ld2420: uart_id: ld2420_uart lcd_menu: + id: test_lcd_menu display_id: my_lcd_gpio mark_back: 0x5e mark_selected: 0x3e @@ -4028,7 +4041,7 @@ lcd_menu: text: Show Main on_value: then: - - display_menu.show_main: + - display_menu.show_main: test_lcd_menu - type: select text: Enum Item immediate_edit: true @@ -4058,7 +4071,7 @@ lcd_menu: text: Hide on_value: then: - - display_menu.hide: + - display_menu.hide: test_lcd_menu - type: switch text: Switch switch: my_switch @@ -4078,6 +4091,91 @@ lcd_menu: then: 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: - platform: template id: alarmcontrolpanel1