ESPHome  2024.4.2
graphical_display_menu.cpp
Go to the documentation of this file.
2 #include "esphome/core/hal.h"
3 #include "esphome/core/helpers.h"
4 #include "esphome/core/log.h"
5 #include <cstdlib>
7 
8 namespace esphome {
9 namespace graphical_display_menu {
10 
11 static const char *const TAG = "graphical_display_menu";
12 
14  if (this->display_ != nullptr) {
15  display::display_writer_t writer = [this](display::Display &it) { this->draw_menu(); };
16  this->display_page_ = make_unique<display::DisplayPage>(writer);
17  }
18 
19  if (!this->menu_item_value_.has_value()) {
20  this->menu_item_value_ = [](const MenuItemValueArguments *it) {
21  std::string label = " ";
22  if (it->is_item_selected && it->is_menu_editing) {
23  label.append(">");
24  label.append(it->item->get_value_text());
25  label.append("<");
26  } else {
27  label.append("(");
28  label.append(it->item->get_value_text());
29  label.append(")");
30  }
31  return label;
32  };
33  }
34 
36 }
37 
39  ESP_LOGCONFIG(TAG, "Graphical Display Menu");
40  ESP_LOGCONFIG(TAG, "Has Display: %s", YESNO(this->display_ != nullptr));
41  ESP_LOGCONFIG(TAG, "Popup Mode: %s", YESNO(this->display_ != nullptr));
42  ESP_LOGCONFIG(TAG, "Advanced Drawing Mode: %s", YESNO(this->display_ == nullptr));
43  ESP_LOGCONFIG(TAG, "Has Font: %s", YESNO(this->font_ != nullptr));
44  ESP_LOGCONFIG(TAG, "Mode: %s", this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick");
45  ESP_LOGCONFIG(TAG, "Active: %s", YESNO(this->active_));
46  ESP_LOGCONFIG(TAG, "Menu items:");
47  for (size_t i = 0; i < this->displayed_item_->items_size(); i++) {
48  auto *item = this->displayed_item_->get_item(i);
49  ESP_LOGCONFIG(TAG, " %i: %s (Type: %s, Immediate Edit: %s)", i, item->get_text().c_str(),
50  LOG_STR_ARG(display_menu_base::menu_item_type_to_string(item->get_type())),
51  YESNO(item->get_immediate_edit()));
52  }
53 }
54 
55 void GraphicalDisplayMenu::set_display(display::Display *display) { this->display_ = display; }
56 
58 
59 void GraphicalDisplayMenu::set_foreground_color(Color foreground_color) { this->foreground_color_ = foreground_color; }
60 void GraphicalDisplayMenu::set_background_color(Color background_color) { this->background_color_ = background_color; }
61 
63  if (this->display_ != nullptr) {
65  this->display_->show_page(this->display_page_.get());
66  this->display_->clear();
67  } else {
68  this->update();
69  }
70 }
71 
73  if (this->previous_display_page_ != nullptr) {
75  this->display_->clear();
76  this->update();
77  this->previous_display_page_ = nullptr;
78  } else {
79  this->update();
80  }
81 }
82 
84  this->update();
85 
86  // If we're in advanced drawing mode we won't have a display and will instead require the update callback to do
87  // our drawing
88  if (this->display_ != nullptr) {
89  draw_menu();
90  }
91 }
92 
94  if (this->display_ == nullptr) {
95  ESP_LOGE(TAG, "draw_menu() called without a display_. This is only available when using the menu in pop up mode");
96  return;
97  }
98  display::Rect bounds(0, 0, this->display_->get_width(), this->display_->get_height());
99  this->draw_menu_internal_(this->display_, &bounds);
100 }
101 
103  this->draw_menu_internal_(display, bounds);
104 }
105 
107  int total_height = 0;
108  int y_padding = 2;
109  bool scroll_menu_items = false;
110  std::vector<display::Rect> menu_dimensions;
111  int number_items_fit_to_screen = 0;
112  const int max_item_index = this->displayed_item_->items_size() - 1;
113 
114  for (size_t i = 0; i <= max_item_index; i++) {
115  const auto *item = this->displayed_item_->get_item(i);
116  const bool selected = i == this->cursor_index_;
117  const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected);
118 
119  menu_dimensions.push_back(item_dimensions);
120  total_height += item_dimensions.h + (i == 0 ? 0 : y_padding);
121 
122  if (total_height <= bounds->h) {
123  number_items_fit_to_screen++;
124  } else {
125  // Scroll the display if the selected item or the item immediately after it overflows
126  if ((selected) || (i == this->cursor_index_ + 1)) {
127  scroll_menu_items = true;
128  }
129  }
130  }
131 
132  // Determine what items to draw
133  int first_item_index = 0;
134  int last_item_index = max_item_index;
135 
136  if (number_items_fit_to_screen <= 1) {
137  // If only one item can fit to the bounds draw the current cursor item
138  last_item_index = std::min(last_item_index, this->cursor_index_ + 1);
139  first_item_index = this->cursor_index_;
140  } else {
141  if (scroll_menu_items) {
142  // Attempt to draw the item after the current item (+1 for equality check in the draw loop)
143  last_item_index = std::min(last_item_index, this->cursor_index_ + 1);
144 
145  // Go back through the measurements to determine how many prior items we can fit
146  int height_left_to_use = bounds->h;
147  for (int i = last_item_index; i >= 0; i--) {
148  const display::Rect item_dimensions = menu_dimensions[i];
149  height_left_to_use -= (item_dimensions.h + y_padding);
150 
151  if (height_left_to_use <= 0) {
152  // Ran out of space - this is our first item to draw
153  first_item_index = i;
154  break;
155  }
156  }
157  const int items_to_draw = last_item_index - first_item_index;
158  // Dont't draw last item partially if it is the selected item
159  if ((this->cursor_index_ == last_item_index) && (number_items_fit_to_screen <= items_to_draw) &&
160  (first_item_index < max_item_index)) {
161  first_item_index++;
162  }
163  }
164  }
165 
166  // Render the items into the view port
167  display->start_clipping(*bounds);
168 
169  int y_offset = bounds->y;
170  for (size_t i = first_item_index; i <= last_item_index; i++) {
171  const auto *item = this->displayed_item_->get_item(i);
172  const bool selected = i == this->cursor_index_;
173  display::Rect dimensions = menu_dimensions[i];
174 
175  dimensions.y = y_offset;
176  dimensions.x = bounds->x;
177  this->draw_item(display, item, &dimensions, selected);
178 
179  y_offset = dimensions.y + dimensions.h + y_padding;
180  }
181 
182  display->end_clipping();
183 }
184 
186  const display::Rect *bounds, const bool selected) {
187  display::Rect dimensions(0, 0, 0, 0);
188 
189  if (selected) {
190  // TODO: Support selection glyph
191  dimensions.w += 0;
192  dimensions.h += 0;
193  }
194 
195  std::string label = item->get_text();
196  if (item->has_value()) {
197  // Append to label
198  MenuItemValueArguments args(item, selected, this->editing_);
199  label.append(this->menu_item_value_.value(&args));
200  }
201 
202  int x1;
203  int y1;
204  int width;
205  int height;
206  display->get_text_bounds(0, 0, label.c_str(), this->font_, display::TextAlign::TOP_LEFT, &x1, &y1, &width, &height);
207 
208  dimensions.w = std::min((int16_t) width, bounds->w);
209  dimensions.h = std::min((int16_t) height, bounds->h);
210 
211  return dimensions;
212 }
213 
215  const display::Rect *bounds, const bool selected) {
216  const auto background_color = selected ? this->foreground_color_ : this->background_color_;
217  const auto foreground_color = selected ? this->background_color_ : this->foreground_color_;
218 
219  // int background_width = std::max(bounds->width, available_width);
220  int background_width = bounds->w;
221 
222  if (selected) {
223  display->filled_rectangle(bounds->x, bounds->y, background_width, bounds->h, background_color);
224  }
225 
226  std::string label = item->get_text();
227  if (item->has_value()) {
228  MenuItemValueArguments args(item, selected, this->editing_);
229  label.append(this->menu_item_value_.value(&args));
230  }
231 
232  display->print(bounds->x, bounds->y, this->font_, foreground_color, display::TextAlign::TOP_LEFT, label.c_str());
233 }
234 
235 void GraphicalDisplayMenu::draw_item(const display_menu_base::MenuItem *item, const uint8_t row, const bool selected) {
236  ESP_LOGE(TAG, "draw_item(MenuItem *item, uint8_t row, bool selected) called. The graphical_display_menu specific "
237  "draw_item should be called.");
238 }
239 
241 
242 } // namespace graphical_display_menu
243 } // namespace esphome
void get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, int *width, int *height)
Get the text bounds of the given string.
Definition: display.cpp:405
virtual bool has_value() const
Definition: menu_item.h:53
int16_t x
X coordinate of corner.
Definition: rect.h:12
void filled_rectangle(int x1, int y1, int width, int height, Color color=COLOR_ON)
Fill a rectangle with the top left point at [x1,y1] and the bottom right point at [x1+width...
Definition: display.cpp:104
void draw_menu_internal_(display::Display *display, const display::Rect *bounds)
const DisplayPage * get_active_page() const
Definition: display.h:570
int16_t h
Height of region.
Definition: rect.h:15
virtual int get_width()
Get the calculated width of the display in pixels with rotation applied.
Definition: display.h:215
int16_t y
Y coordinate of corner.
Definition: rect.h:13
void draw_item(const display_menu_base::MenuItem *item, uint8_t row, bool selected) override
std::function< void(Display &)> display_writer_t
Definition: display.h:180
virtual void setup()
Where the component&#39;s initialization should happen.
Definition: component.cpp:48
void start_clipping(Rect rect)
Set the clipping rectangle for further drawing.
Definition: display.cpp:537
void clear()
Clear the entire screen by filling it with OFF pixels.
Definition: display.cpp:16
void print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text, Color background=COLOR_OFF)
Print text with the anchor point at [x,y] with font.
Definition: display.cpp:337
TemplatableValue< std::string, const MenuItemValueArguments * > menu_item_value_
std::string get_text() const
Definition: menu_item.h:51
void end_clipping()
Reset the invalidation region.
Definition: display.cpp:544
virtual int get_height()
Get the calculated height of the display in pixels with rotation applied.
Definition: display.h:217
std::unique_ptr< display::DisplayPage > display_page_
uint8_t h
Definition: bl0939.h:21
virtual display::Rect measure_item(display::Display *display, const display_menu_base::MenuItem *item, const display::Rect *bounds, bool selected)
This is a workaround until we can figure out a way to get the tflite-micro idf component code availab...
Definition: a01nyub.cpp:7
void show_page(DisplayPage *page)
Definition: display.cpp:496
int16_t w
Width of region.
Definition: rect.h:14
const LogString * menu_item_type_to_string(MenuItemType type)
Returns a string representation of a menu item type suitable for logging.
Definition: menu_item.cpp:8