diff --git a/esphome/components/graphical_layout/__init__.py b/esphome/components/graphical_layout/__init__.py index 12c0797fab..d07725bfc9 100644 --- a/esphome/components/graphical_layout/__init__.py +++ b/esphome/components/graphical_layout/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_TYPE +from esphome.components import color from . import horizontal_stack from . import vertical_stack from . import text_panel @@ -18,8 +19,19 @@ AUTO_LOAD = ["display"] MULTI_CONF = True CONF_LAYOUT = "layout" +CONF_MARGIN = "margin" +CONF_PADDING = "padding" +CONF_BORDER = "border" +CONF_BORDER_COLOR = "border_color" -BASE_ITEM_SCHEMA = cv.Schema({}) +BASE_ITEM_SCHEMA = cv.Schema( + { + cv.Optional(CONF_MARGIN, default=0): cv.templatable(cv.int_range(min=0)), + cv.Optional(CONF_BORDER, default=0): cv.templatable(cv.int_range(min=0)), + cv.Optional(CONF_BORDER_COLOR): cv.use_id(color.ColorStruct), + cv.Optional(CONF_PADDING, default=0): cv.templatable(cv.int_range(min=0)), + } +) def item_type_schema(value): @@ -58,6 +70,25 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) +async def build_layout_item_pvariable(config): + var = cg.new_Pvariable(config[CONF_ID]) + + margin = await cg.templatable(config[CONF_MARGIN], args=[], output_type=int) + cg.add(var.set_margin(margin)) + + border = await cg.templatable(config[CONF_BORDER], args=[], output_type=int) + cg.add(var.set_border(border)) + + if border_color_config := config.get(CONF_BORDER_COLOR): + border_color = await cg.get_variable(border_color_config) + cg.add(var.set_border_color(border_color)) + + padding = await cg.templatable(config[CONF_PADDING], args=[], output_type=int) + cg.add(var.set_margin(padding)) + + return var + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -65,7 +96,9 @@ async def to_code(config): layout_config = config[CONF_LAYOUT] layout_type = layout_config[CONF_TYPE] if layout_type in CODE_GENERATORS: - layout_var = await CODE_GENERATORS[layout_type](layout_config, CODE_GENERATORS) + layout_var = await CODE_GENERATORS[layout_type]( + build_layout_item_pvariable, layout_config, CODE_GENERATORS + ) cg.add(var.set_layout_root(layout_var)) else: raise f"Do not know how to build type {layout_type}" diff --git a/esphome/components/graphical_layout/display_rendering_panel.cpp b/esphome/components/graphical_layout/display_rendering_panel.cpp index 5218c72fe9..904a89aa93 100644 --- a/esphome/components/graphical_layout/display_rendering_panel.cpp +++ b/esphome/components/graphical_layout/display_rendering_panel.cpp @@ -14,11 +14,11 @@ void DisplayRenderingPanel::dump_config(int indent_depth, int additional_level_d ESP_LOGCONFIG(TAG, "%*sHas drawing lambda: %s", indent_depth, "", YESNO(this->lambda_ != nullptr)); } -display::Rect DisplayRenderingPanel::measure_item(display::Display *display) { - return display::Rect(0, 0, this->width_, this->width_); +display::Rect DisplayRenderingPanel::measure_item_internal(display::Display *display) { + return display::Rect(0, 0, this->width_, this->height_); } -void DisplayRenderingPanel::render(display::Display *display, display::Rect bounds) { this->lambda_(*display); } +void DisplayRenderingPanel::render_internal(display::Display *display, display::Rect bounds) { this->lambda_(*display); } } // namespace graphical_layout } // namespace esphome diff --git a/esphome/components/graphical_layout/display_rendering_panel.h b/esphome/components/graphical_layout/display_rendering_panel.h index a48b2b393e..0a753021bf 100644 --- a/esphome/components/graphical_layout/display_rendering_panel.h +++ b/esphome/components/graphical_layout/display_rendering_panel.h @@ -16,8 +16,8 @@ using display_writer_t = std::function; */ class DisplayRenderingPanel : public LayoutItem { public: - display::Rect measure_item(display::Display *display) override; - void render(display::Display *display, display::Rect bounds) override; + display::Rect measure_item_internal(display::Display *display) override; + void render_internal(display::Display *display, display::Rect bounds) override; void dump_config(int indent_depth, int additional_level_depth) override; void set_width(int width) { this->width_ = width; }; diff --git a/esphome/components/graphical_layout/display_rendering_panel.py b/esphome/components/graphical_layout/display_rendering_panel.py index c44c154888..c43effaee2 100644 --- a/esphome/components/graphical_layout/display_rendering_panel.py +++ b/esphome/components/graphical_layout/display_rendering_panel.py @@ -20,8 +20,8 @@ def get_config_schema(base_item_schema, item_type_schema): ) -async def config_to_layout_item(item_config, child_item_builder): - var = cg.new_Pvariable(item_config[CONF_ID]) +async def config_to_layout_item(pvariable_builder, item_config, child_item_builder): + var = await pvariable_builder(item_config) width = await cg.templatable(item_config[CONF_WIDTH], args=[], output_type=int) cg.add(var.set_width(width)) diff --git a/esphome/components/graphical_layout/horizontal_stack.cpp b/esphome/components/graphical_layout/horizontal_stack.cpp index 24726db042..4c7a341c8e 100644 --- a/esphome/components/graphical_layout/horizontal_stack.cpp +++ b/esphome/components/graphical_layout/horizontal_stack.cpp @@ -18,7 +18,7 @@ void HorizontalStack::dump_config(int indent_depth, int additional_level_depth) } } -display::Rect HorizontalStack::measure_item(display::Display *display) { +display::Rect HorizontalStack::measure_item_internal(display::Display *display) { display::Rect rect(this->item_padding_, 0, 0, 0); for (LayoutItem *child : this->children_) { @@ -35,7 +35,7 @@ display::Rect HorizontalStack::measure_item(display::Display *display) { return rect; } -void HorizontalStack::render(display::Display *display, display::Rect bounds) { +void HorizontalStack::render_internal(display::Display *display, display::Rect bounds) { int width_offset = this->item_padding_; for (LayoutItem *item : this->children_) { diff --git a/esphome/components/graphical_layout/horizontal_stack.h b/esphome/components/graphical_layout/horizontal_stack.h index f8b017f055..57fbebcf24 100644 --- a/esphome/components/graphical_layout/horizontal_stack.h +++ b/esphome/components/graphical_layout/horizontal_stack.h @@ -11,8 +11,8 @@ namespace graphical_layout { */ class HorizontalStack : public ContainerLayoutItem { public: - display::Rect measure_item(display::Display *display) override; - void render(display::Display *display, display::Rect bounds) override; + display::Rect measure_item_internal(display::Display *display) override; + void render_internal(display::Display *display, display::Rect bounds) override; void dump_config(int indent_depth, int additional_level_depth) override; void set_item_padding(int item_padding) { this->item_padding_ = item_padding; }; diff --git a/esphome/components/graphical_layout/horizontal_stack.py b/esphome/components/graphical_layout/horizontal_stack.py index 3b755073f5..c42ceffdfa 100644 --- a/esphome/components/graphical_layout/horizontal_stack.py +++ b/esphome/components/graphical_layout/horizontal_stack.py @@ -22,8 +22,8 @@ def get_config_schema(base_item_schema, item_type_schema): ) -async def config_to_layout_item(item_config, child_item_builder): - var = cg.new_Pvariable(item_config[CONF_ID]) +async def config_to_layout_item(pvariable_builder, item_config, child_item_builder): + var = await pvariable_builder(item_config) if item_padding_config := item_config[CONF_ITEM_PADDING]: cg.add(var.set_item_padding(item_padding_config)) @@ -32,7 +32,7 @@ async def config_to_layout_item(item_config, child_item_builder): child_item_type = child_item_config[CONF_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 + pvariable_builder, child_item_config, child_item_builder ) cg.add(var.add_item(child_item_var)) else: diff --git a/esphome/components/graphical_layout/layout_item.cpp b/esphome/components/graphical_layout/layout_item.cpp new file mode 100644 index 0000000000..77fe139b71 --- /dev/null +++ b/esphome/components/graphical_layout/layout_item.cpp @@ -0,0 +1,60 @@ +#include "layout_item.h" + +#include "esphome/components/display/display.h" +#include "esphome/components/display/rect.h" + +namespace esphome { +namespace graphical_layout { + +static const char *const TAG = "layoutitem"; + +display::Rect LayoutItem::measure_item(display::Display *display) { + display::Rect inner_size = this->measure_item_internal(display); + int margin_border_padding = this->margin_ + this->border_ + this->padding_; + + return display::Rect(0, 0, (margin_border_padding * 2) + inner_size.w, (margin_border_padding * 2) + inner_size.h); +} + +void LayoutItem::render(display::Display *display, display::Rect bounds) { + // Margin + display->set_local_coordinates_relative_to_current(this->margin_, this->margin_); + + // Border + if (this->border_ > 0) { + display::Rect border_bounds(0, 0, bounds.w - (this->margin_ * 2), bounds.h - (this->margin_ * 2)); + if (this->border_ == 1) { + // Single pixel border use the native function + display->rectangle(0, 0, border_bounds.w, border_bounds.h, this->border_color_); + } else { + // Thicker border need to do mutiple filled rects + // Top rectangle + display->filled_rectangle(border_bounds.x, border_bounds.y, border_bounds.w, this->border_); + // Bottom rectangle + display->filled_rectangle(border_bounds.x, border_bounds.h - this->border_, border_bounds.w, this->border_); + // Left rectangle + display->filled_rectangle(border_bounds.x, border_bounds.y, this->border_, border_bounds.h); + // Right rectangle + display->filled_rectangle(border_bounds.w - this->border_, border_bounds.y, this->border_, border_bounds.h); + } + } + + // Padding + display->set_local_coordinates_relative_to_current(this->border_ + this->padding_, this->border_ + this->padding_); + int margin_border_padding_offset = (this->margin_ + this->border_ + this->padding_) * 2; + display::Rect internal_bounds(0, 0, bounds.w - margin_border_padding_offset, bounds.h - margin_border_padding_offset); + + // Rendering + this->render_internal(display, internal_bounds); + + // Pop padding coords + display->pop_local_coordinates(); + + // Border doesn't use local coords + + // Pop margin coords + display->pop_local_coordinates(); +} + + +} // namespace graphical_layout +} // namespace esphome diff --git a/esphome/components/graphical_layout/layout_item.h b/esphome/components/graphical_layout/layout_item.h index aeeb3035d7..3cdd49c3fc 100644 --- a/esphome/components/graphical_layout/layout_item.h +++ b/esphome/components/graphical_layout/layout_item.h @@ -1,5 +1,7 @@ #pragma once +#include "esphome/core/color.h" + namespace esphome { namespace display { class Display; @@ -11,25 +13,56 @@ 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 + /** Measures the item as it would be drawn on the display and returns the bounds for it. This should + * include any margin and padding. It is rare you will need to override this unless you are doing + * something non-standard with margins and padding * * param[in] display: Display that will be used for rendering. May be used to help with calculations */ - virtual display::Rect measure_item(display::Display *display) = 0; + virtual display::Rect measure_item(display::Display *display); - /** Perform the rendering of the item to the display + /** Measures the internal size of the item this should only be the portion drawn exclusive + * of any padding or margins + * + * param[in] display: Display that will be used for rendering. May be used to help with calculations + */ + virtual display::Rect measure_item_internal(display::Display *display) = 0; + + /** Perform the rendering of the item to the display accounting for the margin and padding of the + * item. It is rare you will need to override this unless you are doing something non-standard with + * margins and padding * * 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; + virtual void render(display::Display *display, display::Rect bounds); - /** + /** Performs the rendering of the item internals of the item exclusive of any padding or margins + * (or rather, after they've already been handled by render) + * + * param[in] display: Display to render to + * param[in] bounds: Size of the area drawing should be constrained to + */ + virtual void render_internal(display::Display *display, display::Rect bounds) = 0; + + /** Dump the items config to aid the user + * * 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; + + void set_margin(int margin) { this->margin_ = margin; }; + void set_padding(int padding) { this->padding_ = padding; }; + void set_border(int border) { this->border_ = border; }; + void set_border_color(Color color) { this->border_color_ = color; }; + + protected: + int margin_{0}; + int padding_{0}; + int border_{0}; + Color border_color_{Color(0, 0, 0, 0)}; }; } // namespace graphical_layout diff --git a/esphome/components/graphical_layout/text_panel.cpp b/esphome/components/graphical_layout/text_panel.cpp index 228c8432ef..71c5caec20 100644 --- a/esphome/components/graphical_layout/text_panel.cpp +++ b/esphome/components/graphical_layout/text_panel.cpp @@ -13,7 +13,7 @@ void TextPanel::dump_config(int indent_depth, int additional_level_depth) { ESP_LOGCONFIG(TAG, "%*sText: %s", indent_depth, "", this->text_.c_str()); } -display::Rect TextPanel::measure_item(display::Display *display) { +display::Rect TextPanel::measure_item_internal(display::Display *display) { int x1; int y1; int width; @@ -25,7 +25,7 @@ display::Rect TextPanel::measure_item(display::Display *display) { return display::Rect(0, 0, width, height); } -void TextPanel::render(display::Display *display, display::Rect bounds) { +void TextPanel::render_internal(display::Display *display, display::Rect bounds) { display->print(0, 0, this->font_, this->foreground_color_, display::TextAlign::TOP_LEFT, this->text_.c_str()); } diff --git a/esphome/components/graphical_layout/text_panel.h b/esphome/components/graphical_layout/text_panel.h index 5c0d7832ec..8a5c1e7315 100644 --- a/esphome/components/graphical_layout/text_panel.h +++ b/esphome/components/graphical_layout/text_panel.h @@ -14,8 +14,8 @@ 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: - display::Rect measure_item(display::Display *display) override; - void render(display::Display *display, display::Rect bounds) override; + display::Rect measure_item_internal(display::Display *display) override; + void render_internal(display::Display *display, display::Rect bounds) override; void dump_config(int indent_depth, int additional_level_depth) override; void set_item_padding(int item_padding) { this->item_padding_ = item_padding; }; diff --git a/esphome/components/graphical_layout/text_panel.py b/esphome/components/graphical_layout/text_panel.py index 88709424e2..9d6e272523 100644 --- a/esphome/components/graphical_layout/text_panel.py +++ b/esphome/components/graphical_layout/text_panel.py @@ -27,8 +27,8 @@ def get_config_schema(base_item_schema, item_type_schema): ) -async def config_to_layout_item(item_config, child_item_builder): - var = cg.new_Pvariable(item_config[CONF_ID]) +async def config_to_layout_item(pvariable_builder, item_config, child_item_builder): + var = await pvariable_builder(item_config) if item_padding_config := item_config[CONF_ITEM_PADDING]: cg.add(var.set_item_padding(item_padding_config)) diff --git a/esphome/components/graphical_layout/vertical_stack.cpp b/esphome/components/graphical_layout/vertical_stack.cpp index 09ef685ab2..bb357c68cf 100644 --- a/esphome/components/graphical_layout/vertical_stack.cpp +++ b/esphome/components/graphical_layout/vertical_stack.cpp @@ -7,7 +7,7 @@ namespace esphome { namespace graphical_layout { -static const char *TAG = "verticalstack"; +static const char *const TAG = "verticalstack"; void VerticalStack::dump_config(int indent_depth, int additional_level_depth) { ESP_LOGCONFIG(TAG, "%*sItem Padding: %i", indent_depth, "", this->item_padding_); @@ -18,7 +18,7 @@ void VerticalStack::dump_config(int indent_depth, int additional_level_depth) { } } -display::Rect VerticalStack::measure_item(display::Display *display) { +display::Rect VerticalStack::measure_item_internal(display::Display *display) { display::Rect rect(0, this->item_padding_, 0, 0); for (LayoutItem *child : this->children_) { @@ -33,7 +33,7 @@ display::Rect VerticalStack::measure_item(display::Display *display) { return rect; } -void VerticalStack::render(display::Display *display, display::Rect bounds) { +void VerticalStack::render_internal(display::Display *display, display::Rect bounds) { int height_offset = this->item_padding_; for (LayoutItem *item : this->children_) { diff --git a/esphome/components/graphical_layout/vertical_stack.h b/esphome/components/graphical_layout/vertical_stack.h index df89b63f2b..2c21f539a9 100644 --- a/esphome/components/graphical_layout/vertical_stack.h +++ b/esphome/components/graphical_layout/vertical_stack.h @@ -10,8 +10,8 @@ namespace graphical_layout { */ class VerticalStack : public ContainerLayoutItem { public: - display::Rect measure_item(display::Display *display) override; - void render(display::Display *display, display::Rect bounds) override; + display::Rect measure_item_internal(display::Display *display) override; + void render_internal(display::Display *display, display::Rect bounds) override; void dump_config(int indent_depth, int additional_level_depth) override; void set_item_padding(int item_padding) { this->item_padding_ = item_padding; }; diff --git a/esphome/components/graphical_layout/vertical_stack.py b/esphome/components/graphical_layout/vertical_stack.py index 1ea566f16b..9497819aba 100644 --- a/esphome/components/graphical_layout/vertical_stack.py +++ b/esphome/components/graphical_layout/vertical_stack.py @@ -22,8 +22,8 @@ def get_config_schema(base_item_schema, item_type_schema): ) -async def config_to_layout_item(item_config, child_item_builder): - var = cg.new_Pvariable(item_config[CONF_ID]) +async def config_to_layout_item(pvariable_builder, item_config, child_item_builder): + var = await pvariable_builder(item_config) if item_padding_config := item_config[CONF_ITEM_PADDING]: cg.add(var.set_item_padding(item_padding_config)) @@ -32,7 +32,7 @@ async def config_to_layout_item(item_config, child_item_builder): child_item_type = child_item_config[CONF_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 + pvariable_builder, child_item_config, child_item_builder ) cg.add(var.add_item(child_item_var)) else: