From 399bbe29e836d865f12c667c09f25b093d678721 Mon Sep 17 00:00:00 2001 From: Michael Davidson Date: Sun, 17 Dec 2023 20:19:16 +1100 Subject: [PATCH] Initial check in of a graphical layout system WIP: But supports vertical and horizontal stacking of items and a simple text item --- CODEOWNERS | 1 + esphome/components/display/display.cpp | 7 ++ esphome/components/display/display.h | 15 +++ .../components/graphical_layout/__init__.py | 91 +++++++++++++++++++ .../graphical_layout/container_layout_item.h | 29 ++++++ .../graphical_layout/graphical_layout.cpp | 37 ++++++++ .../graphical_layout/graphical_layout.h | 38 ++++++++ .../graphical_layout/horizontal_stack.cpp | 51 +++++++++++ .../graphical_layout/horizontal_stack.h | 25 +++++ .../graphical_layout/horizontal_stack.py | 27 ++++++ .../components/graphical_layout/layout_item.h | 37 ++++++++ .../graphical_layout/layoutexport.py | 9 ++ .../graphical_layout/text_panel.cpp | 32 +++++++ .../components/graphical_layout/text_panel.h | 36 ++++++++ .../components/graphical_layout/text_panel.py | 48 ++++++++++ .../graphical_layout/vertical_stack.cpp | 50 ++++++++++ .../graphical_layout/vertical_stack.h | 25 +++++ .../graphical_layout/vertical_stack.py | 28 ++++++ esphome/core/defines.h | 1 + 19 files changed, 587 insertions(+) create mode 100644 esphome/components/graphical_layout/__init__.py create mode 100644 esphome/components/graphical_layout/container_layout_item.h create mode 100644 esphome/components/graphical_layout/graphical_layout.cpp create mode 100644 esphome/components/graphical_layout/graphical_layout.h create mode 100644 esphome/components/graphical_layout/horizontal_stack.cpp create mode 100644 esphome/components/graphical_layout/horizontal_stack.h create mode 100644 esphome/components/graphical_layout/horizontal_stack.py create mode 100644 esphome/components/graphical_layout/layout_item.h create mode 100644 esphome/components/graphical_layout/layoutexport.py create mode 100644 esphome/components/graphical_layout/text_panel.cpp create mode 100644 esphome/components/graphical_layout/text_panel.h create mode 100644 esphome/components/graphical_layout/text_panel.py create mode 100644 esphome/components/graphical_layout/vertical_stack.cpp create mode 100644 esphome/components/graphical_layout/vertical_stack.h create mode 100644 esphome/components/graphical_layout/vertical_stack.py diff --git a/CODEOWNERS b/CODEOWNERS index de80806eac..ebccbe61b2 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_layout/* @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 49c9d8a80a..b1e2fa1b22 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_LAYOUT + void Display::render_layout(int x, int y, graphical_layout::RootLayoutComponent *layout) { + display::Rect b2(x, y, 100, 100); + layout->render_at(this, x, y); + } +#endif + 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 0e920aef1c..84689cdaf8 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -18,6 +18,10 @@ #include "esphome/components/qr_code/qr_code.h" #endif +#ifdef USE_GRAPHICAL_LAYOUT +#include "esphome/components/graphical_layout/graphical_layout.h" +#endif + namespace esphome { namespace display { @@ -393,6 +397,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_LAYOUT + /** Draw the graphical layout with the top corner at [x,y] + * + * @param x The x coordinate of the upper left corner + * @param y The y coordinate of the upper left corner + * @param layout The graphical layout to render + * + */ + void render_layout(int x, int y, graphical_layout::RootLayoutComponent *layout); +#endif + /** 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/graphical_layout/__init__.py b/esphome/components/graphical_layout/__init__.py new file mode 100644 index 0000000000..d8ae49329d --- /dev/null +++ b/esphome/components/graphical_layout/__init__.py @@ -0,0 +1,91 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +import esphome.components.graphical_layout.horizontal_stack as horizontal_stack +import esphome.components.graphical_layout.vertical_stack as vertical_stack +import esphome.components.graphical_layout.text_panel as text_panel +from esphome.components import font, color +from esphome.const import CONF_ID + +graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout") +RootLayoutComponent = graphical_layout_ns.class_("RootLayoutComponent", cg.Component) +LayoutItem = graphical_layout_ns.class_("LayoutItem") +ContainerLayoutItem = graphical_layout_ns.class_("ContainerLayoutItem", LayoutItem) + +CODEOWNERS = ["@MrMDavidson"] + +AUTO_LOAD = ["display"] + +MULTI_CONF = True + +CONF_ITEMS = "items" +CONF_LAYOUT = "layout" +CONF_ITEM_TYPE = "type" + +BASE_ITEM_SCHEMA = cv.Schema( + { + } +) + +def item_type_schema(value): + return ITEM_TYPE_SCHEMA(value) + +ITEM_TYPE_SCHEMA = cv.typed_schema( + { + text_panel.CONF_TYPE: cv.Schema( + { + cv.GenerateID(): cv.declare_id(text_panel.TextPanel), + cv.Optional(text_panel.CONF_ITEM_PADDING, default=0): cv.templatable(cv.int_), + cv.Required(text_panel.CONF_FONT): cv.use_id(font.Font), + cv.Optional(text_panel.CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct), + cv.Optional(text_panel.CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct), + cv.Required(text_panel.CONF_TEXT): cv.templatable(cv.string), + } + ), + horizontal_stack.CONF_TYPE: BASE_ITEM_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(horizontal_stack.HorizontalStack), + cv.Optional(horizontal_stack.CONF_ITEM_PADDING, default=0): cv.templatable(cv.int_), + cv.Required(CONF_ITEMS): cv.All( + cv.ensure_list(item_type_schema), cv.Length(min=1) + ) + } + ), + vertical_stack.CONF_TYPE: BASE_ITEM_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(vertical_stack.VerticalStack), + cv.Optional(vertical_stack.CONF_ITEM_PADDING, default=0): cv.templatable(cv.int_), + cv.Required(CONF_ITEMS): cv.All( + cv.ensure_list(item_type_schema), cv.Length(min=1) + ) + } + ) + } +) + +CODE_GENERATORS = { + text_panel.CONF_TYPE: text_panel.config_to_layout_item, + horizontal_stack.CONF_TYPE: horizontal_stack.config_to_layout_item, + vertical_stack.CONF_TYPE: vertical_stack.config_to_layout_item +} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(RootLayoutComponent), + cv.Required(CONF_LAYOUT): ITEM_TYPE_SCHEMA + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + layout_config = config[CONF_LAYOUT] + layout_type = layout_config[CONF_ITEM_TYPE] + if layout_type in CODE_GENERATORS: + layout_var = await CODE_GENERATORS[layout_type](layout_config, CODE_GENERATORS) + cg.add(var.set_layout_root(layout_var)) + else: + raise f"Do not know how to build type {layout_type}" + + cg.add_define("USE_GRAPHICAL_LAYOUT") diff --git a/esphome/components/graphical_layout/container_layout_item.h b/esphome/components/graphical_layout/container_layout_item.h new file mode 100644 index 0000000000..1832810c31 --- /dev/null +++ b/esphome/components/graphical_layout/container_layout_item.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/components/graphical_layout/layout_item.h" + +namespace esphome { +namespace display { +class Display; +class Rect; +} + +namespace graphical_layout { + +/** The ContainerLayoutItem can be used to derive from when a layout item has children. + * It does not define what or how child items get used just that they exist for the item +*/ +class ContainerLayoutItem : public LayoutItem { + public: + + /** Adds an item to this container */ + void add_item(LayoutItem *child) { + this->children_.push_back(child); + } + + protected: + std::vector children_; +}; + +} +} \ No newline at end of file diff --git a/esphome/components/graphical_layout/graphical_layout.cpp b/esphome/components/graphical_layout/graphical_layout.cpp new file mode 100644 index 0000000000..9c9a2e1bff --- /dev/null +++ b/esphome/components/graphical_layout/graphical_layout.cpp @@ -0,0 +1,37 @@ +#include "graphical_layout.h" + +#include "esphome/components/display/display.h" +#include "esphome/core/log.h" + + +namespace esphome { +namespace graphical_layout { + +static const char *const TAG = "rootlayoutcomponent"; + +void RootLayoutComponent::setup() { +} + +void RootLayoutComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Graphical Layout"); + this->layout_root_->dump_config(2, 2); +} + +void RootLayoutComponent::render_at(display::Display *display, int x, int y) { + display->set_local_coordinate(x, y); + + display::Rect layout_rect = this->layout_root_->measure_item(display); + display::Rect clipping_rect = display::Rect(x, y, layout_rect.w, layout_rect.h); + + // TODO: Should clipping be relative to local? + display->start_clipping(clipping_rect); + + // Render everything + this->layout_root_->render(display, layout_rect); + + display->pop_local_coordinates(); + display->shrink_clipping(clipping_rect); +} + +} // namespace graphical_layout +} // namespace esphome diff --git a/esphome/components/graphical_layout/graphical_layout.h b/esphome/components/graphical_layout/graphical_layout.h new file mode 100644 index 0000000000..6543a18894 --- /dev/null +++ b/esphome/components/graphical_layout/graphical_layout.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include "esphome/core/component.h" +#include "esphome/components/display/rect.h" +#include "esphome/components/graphical_layout/layout_item.h" + +namespace esphome { +namespace display { +class Display; +class Rect; +} + +namespace graphical_layout { + +/** Component used for rendering the layout*/ +class RootLayoutComponent : public Component { + public: + void setup() override; + void dump_config() override; + + /** Render the graphical layout to the screen + * + * param[in] display: Display that will be rendered to + * param[in] x: x coordinate to render at + * param[in] y: y coorindate to render at + */ + void render_at(display::Display *display, int x, int y); + + void set_layout_root(LayoutItem *layout) { this->layout_root_ = layout; }; + + protected: + LayoutItem *layout_root_{nullptr}; +}; + +} // namespace graphical_layout +} // namespace esphome diff --git a/esphome/components/graphical_layout/horizontal_stack.cpp b/esphome/components/graphical_layout/horizontal_stack.cpp new file mode 100644 index 0000000000..3f1911dd34 --- /dev/null +++ b/esphome/components/graphical_layout/horizontal_stack.cpp @@ -0,0 +1,51 @@ +#include "horizontal_stack.h" + +#include "esphome/components/display/display.h" +#include "esphome/components/display/rect.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace graphical_layout { + +static const char*TAG = "horizontalstack"; + +void HorizontalStack::dump_config(int indent_depth, int additional_level_depth) { + ESP_LOGCONFIG(TAG, "%*sItem Padding: %i", indent_depth, "", this->item_padding_); + ESP_LOGCONFIG(TAG, "%*sChildren: %i", indent_depth, "", this->children_.size()); + + for (LayoutItem *child : this->children_) { + child->dump_config(indent_depth + additional_level_depth, additional_level_depth); + } +} + +const display::Rect HorizontalStack::measure_item(display::Display *display) { + display::Rect rect(this->item_padding_, 0, 0, 0); + + for (LayoutItem *child : this->children_) { + display::Rect child_rect = child->measure_item(display); + rect.h = std::max(rect.h, child_rect.h); + rect.w += child_rect.w + this->item_padding_; + } + + // Add item padding top and bottom + rect.h += (this->item_padding_ * 2); + + ESP_LOGD(TAG, "Measured size is (%i, %i, %i, %i)", rect.x, rect.y, rect.x2(), rect.y2()); + + return rect; +} + +void HorizontalStack::render(display::Display *display, display::Rect bounds) { + int width_offset = this->item_padding_; + + for (LayoutItem *item : this->children_) { + display::Rect measure = item->measure_item(display); + display->set_local_coordinates_relative_to_current(width_offset, this->item_padding_); + item->render(display, measure); + display->pop_local_coordinates(); + width_offset += measure.w + this->item_padding_; + } +} + +} +} \ No newline at end of file diff --git a/esphome/components/graphical_layout/horizontal_stack.h b/esphome/components/graphical_layout/horizontal_stack.h new file mode 100644 index 0000000000..c2192ff68f --- /dev/null +++ b/esphome/components/graphical_layout/horizontal_stack.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/components/graphical_layout/graphical_layout.h" +#include "esphome/components/graphical_layout/container_layout_item.h" + +namespace esphome { +namespace graphical_layout { + +/** + * The HorizontalStack is a UI element which will render a series of items left-to-right across a display +*/ +class HorizontalStack : public ContainerLayoutItem { + public: + const display::Rect measure_item(display::Display *display); + void render(display::Display *display, display::Rect bounds); + + void dump_config(int indent_depth, int additional_level_depth); + void set_item_padding(int item_padding) { this->item_padding_ = item_padding; }; + + protected: + int item_padding_{0}; +}; + +} +} \ No newline at end of file diff --git a/esphome/components/graphical_layout/horizontal_stack.py b/esphome/components/graphical_layout/horizontal_stack.py new file mode 100644 index 0000000000..98f218f3a7 --- /dev/null +++ b/esphome/components/graphical_layout/horizontal_stack.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout") +HorizontalStack = graphical_layout_ns.class_("HorizontalStack") + +CONF_ITEM_PADDING = "item_padding" +CONF_TYPE = "horizontal_stack" +CONF_ITEMS = "items" +CONF_ITEM_TYPE = "type" + +async def config_to_layout_item(item_config, child_item_builder): + var = cg.new_Pvariable(item_config[CONF_ID]) + + if item_padding_config := item_config[CONF_ITEM_PADDING]: + cg.add(var.set_item_padding(item_padding_config)) + + for child_item_config in item_config[CONF_ITEMS]: + child_item_type = child_item_config[CONF_ITEM_TYPE] + if child_item_type in child_item_builder: + child_item_var = await child_item_builder[child_item_type](child_item_config, child_item_builder) + cg.add(var.add_item(child_item_var)) + else: + raise f"Do not know how to build type {child_item_type}" + + return var diff --git a/esphome/components/graphical_layout/layout_item.h b/esphome/components/graphical_layout/layout_item.h new file mode 100644 index 0000000000..117b26fba7 --- /dev/null +++ b/esphome/components/graphical_layout/layout_item.h @@ -0,0 +1,37 @@ +#pragma once + +namespace esphome { +namespace display { +class Display; +class Rect; +} + +namespace graphical_layout { + +/** LayoutItem is the base from which all items derive from*/ +class LayoutItem { + public: + + /** Measures the item as it would be drawn on the display and returns the bounds for it + * + * param[in] display: Display that will be used for rendering. May be used to help with calculations + */ + virtual const display::Rect measure_item(display::Display *display) = 0; + + /** Perform the rendering of the item to the display + * + * param[in] display: Display to render to + * param[in] bounds: Size of the area drawing should be constrained to + */ + virtual void render(display::Display *display, display::Rect bounds) = 0; + + /** + * param[in] indent_depth: Depth to indent the config + * param[in] additional_level_depth: If children require their config to be dumped you increment + * their indent_depth before calling it + */ + virtual void dump_config(int indent_depth, int additional_level_depth) = 0; +}; + +} +} \ No newline at end of file diff --git a/esphome/components/graphical_layout/layoutexport.py b/esphome/components/graphical_layout/layoutexport.py new file mode 100644 index 0000000000..3c69c56277 --- /dev/null +++ b/esphome/components/graphical_layout/layoutexport.py @@ -0,0 +1,9 @@ +import esphome.config_validation as cv +from typing import Awaitable, Any, Callable, Optional + +class LayoutImport: + def __init__(self, name : str, schema_builder_func: Callable[[cv.Schema, cv.Schema, cv.Schema], cv.Schema], builder_func : Awaitable[Any], parent_schema_builder_func : Optional[Callable[[], cv.Schema]] = None): + self.name = name + self.schema_builder_func = schema_builder_func + self.builder_func = builder_func + self.parent_schema_builder_func = parent_schema_builder_func \ No newline at end of file diff --git a/esphome/components/graphical_layout/text_panel.cpp b/esphome/components/graphical_layout/text_panel.cpp new file mode 100644 index 0000000000..b70b94dd7d --- /dev/null +++ b/esphome/components/graphical_layout/text_panel.cpp @@ -0,0 +1,32 @@ +#include "text_panel.h" + +#include "esphome/components/display/display.h" +#include "esphome/components/display/rect.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace graphical_layout { + +static const char *const TAG = "textpanel"; + +void TextPanel::dump_config(int indent_depth, int additional_level_depth) { + ESP_LOGCONFIG(TAG, "%*sText: %s", indent_depth, "", this->text_.c_str()); +} + +const display::Rect TextPanel::measure_item(display::Display *display) { + int x1; + int y1; + int width; + int height; + + display->get_text_bounds(0, 0, this->text_.c_str(), this->font_, display::TextAlign::TOP_LEFT, &x1, &y1, &width, &height); + + return display::Rect(0, 0, width, height); +} + +void TextPanel::render(display::Display *display, display::Rect bounds) { + display->print(0, 0, this->font_, this->foreground_color_, display::TextAlign::TOP_LEFT, this->text_.c_str()); +} + +} +} \ No newline at end of file diff --git a/esphome/components/graphical_layout/text_panel.h b/esphome/components/graphical_layout/text_panel.h new file mode 100644 index 0000000000..e840b8e9d1 --- /dev/null +++ b/esphome/components/graphical_layout/text_panel.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/components/graphical_layout/graphical_layout.h" +#include "esphome/components/font/font.h" + + +namespace esphome { +namespace graphical_layout { + +const Color COLOR_ON(255, 255, 255, 255); +const Color COLOR_OFF(0, 0, 0, 0); + +/** The TextPanel is a UI item that renders a single line of text to a display */ +class TextPanel : public LayoutItem { + public: + const display::Rect measure_item(display::Display *display); + void render(display::Display *display, display::Rect bounds); + void dump_config(int indent_depth, int additional_level_depth); + + void set_item_padding(int item_padding) { this->item_padding_ = item_padding; }; + void set_text(std::string text) { this->text_ = text; }; + void set_font(display::BaseFont *font) { this->font_ = font; }; + void set_foreground_color(Color foreground_color) { this->foreground_color_ = foreground_color; }; + void set_background_color(Color background_color) { this->background_color_ = background_color; }; + + + protected: + int item_padding_{0}; + std::string text_{}; + display::BaseFont *font_{nullptr}; + Color foreground_color_{COLOR_ON}; + Color background_color_{COLOR_OFF}; +}; + +} +} \ No newline at end of file diff --git a/esphome/components/graphical_layout/text_panel.py b/esphome/components/graphical_layout/text_panel.py new file mode 100644 index 0000000000..377f3c8375 --- /dev/null +++ b/esphome/components/graphical_layout/text_panel.py @@ -0,0 +1,48 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import font, color +from esphome.const import CONF_ID + +graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout") +TextPanel = graphical_layout_ns.class_("TextPanel") + +CONF_ITEM_PADDING = "item_padding" +CONF_TYPE = "text_panel" +CONF_FONT = "font" +CONF_FOREGROUND_COLOR = "foreground_color" +CONF_BACKGROUND_COLOR = "background_color" +CONF_TEXT = "text" + +LAYOUT_ITEM_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(TextPanel), + cv.Optional(CONF_ITEM_PADDING, default=0): cv.templatable(cv.int_), + cv.Required(CONF_FONT): cv.use_id(font.Font), + cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct), + cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct), + cv.Required(CONF_TEXT): cv.templatable(cv.string), + } +) + + +async def config_to_layout_item(item_config, child_item_builder): + var = cg.new_Pvariable(item_config[CONF_ID]) + + if item_padding_config := item_config[CONF_ITEM_PADDING]: + cg.add(var.set_item_padding(item_padding_config)) + + font = await cg.get_variable(item_config[CONF_FONT]) + cg.add(var.set_font(font)) + + if foreground_color_config := item_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 := item_config.get(CONF_BACKGROUND_COLOR): + background_color = await cg.get_variable(background_color_config) + cg.add(var.set_background_color(background_color)) + + text = await cg.templatable(item_config[CONF_TEXT], args = [], output_type = str) + cg.add(var.set_text(text)) + + return var \ No newline at end of file diff --git a/esphome/components/graphical_layout/vertical_stack.cpp b/esphome/components/graphical_layout/vertical_stack.cpp new file mode 100644 index 0000000000..eda18f53c1 --- /dev/null +++ b/esphome/components/graphical_layout/vertical_stack.cpp @@ -0,0 +1,50 @@ +#include "vertical_stack.h" + +#include "esphome/components/display/display.h" +#include "esphome/components/display/rect.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace graphical_layout { + +static const char*TAG = "verticalstack"; + +void VerticalStack::dump_config(int indent_depth, int additional_level_depth) { + ESP_LOGCONFIG(TAG, "%*sItem Padding: %i", indent_depth, "", this->item_padding_); + ESP_LOGCONFIG(TAG, "%*sChildren: %i", indent_depth, "", this->children_.size()); + + for (LayoutItem *child : this->children_) { + child->dump_config(indent_depth + additional_level_depth, additional_level_depth); + } +} + +const display::Rect VerticalStack::measure_item(display::Display *display) { + display::Rect rect(0, this->item_padding_, 0, 0); + + for (LayoutItem *child : this->children_) { + display::Rect child_rect = child->measure_item(display); + rect.w = std::max(rect.w, child_rect.w); + rect.h += child_rect.h + this->item_padding_; + } + + // Add item padding left and right + rect.h += (this->item_padding_ * 2); + + return rect; +} + +void VerticalStack::render(display::Display *display, display::Rect bounds) { + int height_offset = this->item_padding_; + + for (LayoutItem *item : this->children_) { + display::Rect measure = item->measure_item(display); + + display->set_local_coordinates_relative_to_current(this->item_padding_, height_offset); + item->render(display, measure); + display->pop_local_coordinates(); + height_offset += measure.h + this->item_padding_; + } +} + +} +} \ No newline at end of file diff --git a/esphome/components/graphical_layout/vertical_stack.h b/esphome/components/graphical_layout/vertical_stack.h new file mode 100644 index 0000000000..412ae230b3 --- /dev/null +++ b/esphome/components/graphical_layout/vertical_stack.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/components/graphical_layout/graphical_layout.h" +#include "esphome/components/graphical_layout/container_layout_item.h" + + +namespace esphome { +namespace graphical_layout { + +/** The HorizontalStack is a UI element which will render a series of items top to bottom down a display + */ +class VerticalStack : public ContainerLayoutItem { + public: + const display::Rect measure_item(display::Display *display); + void render(display::Display *display, display::Rect bounds); + + void dump_config(int indent_depth, int additional_level_depth); + void set_item_padding(int item_padding) { this->item_padding_ = item_padding; }; + + protected: + int item_padding_{0}; +}; + +} +} \ No newline at end of file diff --git a/esphome/components/graphical_layout/vertical_stack.py b/esphome/components/graphical_layout/vertical_stack.py new file mode 100644 index 0000000000..5b5be39f7f --- /dev/null +++ b/esphome/components/graphical_layout/vertical_stack.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout") +VerticalStack = graphical_layout_ns.class_("VerticalStack") + +CONF_ITEM_PADDING = "item_padding" +CONF_TYPE = "vertical_stack" +CONF_ITEMS = "items" +CONF_ITEM_TYPE = "type" + + +async def config_to_layout_item(item_config, child_item_builder): + var = cg.new_Pvariable(item_config[CONF_ID]) + + if item_padding_config := item_config[CONF_ITEM_PADDING]: + cg.add(var.set_item_padding(item_padding_config)) + + for child_item_config in item_config[CONF_ITEMS]: + child_item_type = child_item_config[CONF_ITEM_TYPE] + if child_item_type in child_item_builder: + child_item_var = await child_item_builder[child_item_type](child_item_config, child_item_builder) + cg.add(var.add_item(child_item_var)) + else: + raise f"Do not know how to build type {child_item_type}" + + return var \ No newline at end of file diff --git a/esphome/core/defines.h b/esphome/core/defines.h index b93b8c9270..086abec3ac 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_LAYOUT // Arduino-specific feature flags #ifdef USE_ARDUINO