diff --git a/esphome/components/graphical_layout/horizontal_stack.cpp b/esphome/components/graphical_layout/horizontal_stack.cpp index 4c7a341c8e..5557b7af81 100644 --- a/esphome/components/graphical_layout/horizontal_stack.cpp +++ b/esphome/components/graphical_layout/horizontal_stack.cpp @@ -11,6 +11,7 @@ static const char *const 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, "%*sChild alignment: %i", indent_depth, "", (int)this->child_align_); ESP_LOGCONFIG(TAG, "%*sChildren: %i", indent_depth, "", this->children_.size()); for (LayoutItem *child : this->children_) { @@ -40,8 +41,38 @@ void HorizontalStack::render_internal(display::Display *display, display::Rect b for (LayoutItem *item : this->children_) { display::Rect measure = item->measure_item(display); + bool align_altered_local_coordinates = false; + + switch (this->child_align_) { + case VerticalChildAlign::CENTER_VERTICAL: { + align_altered_local_coordinates = true; + int adjustment = (bounds.h - measure.h) / 2; + display->set_local_coordinates_relative_to_current(0, adjustment); + break; + } + case VerticalChildAlign::BOTTOM: { + align_altered_local_coordinates = true; + display->set_local_coordinates_relative_to_current(0, bounds.h - measure.h); + break; + } + case VerticalChildAlign::STRETCH_TO_FIT_HEIGHT: { + // Items always get the same height as the tallest item + measure.h = bounds.h; + break; + } + case VerticalChildAlign::TOP: + default: { + // No action + break; + } + } + display->set_local_coordinates_relative_to_current(width_offset, this->item_padding_); item->render(display, measure); + if (align_altered_local_coordinates) { + // Additional pop of local coords due to alignment + display->pop_local_coordinates(); + } display->pop_local_coordinates(); width_offset += measure.w + this->item_padding_; } diff --git a/esphome/components/graphical_layout/horizontal_stack.h b/esphome/components/graphical_layout/horizontal_stack.h index 57fbebcf24..ab6a2b6dba 100644 --- a/esphome/components/graphical_layout/horizontal_stack.h +++ b/esphome/components/graphical_layout/horizontal_stack.h @@ -16,9 +16,11 @@ class HorizontalStack : public ContainerLayoutItem { void dump_config(int indent_depth, int additional_level_depth) override; void set_item_padding(int item_padding) { this->item_padding_ = item_padding; }; + void set_child_align(VerticalChildAlign child_align) { this->child_align_ = child_align; }; protected: int item_padding_{0}; + VerticalChildAlign child_align_{VerticalChildAlign::TOP}; }; } // namespace graphical_layout diff --git a/esphome/components/graphical_layout/horizontal_stack.py b/esphome/components/graphical_layout/horizontal_stack.py index c42ceffdfa..39058b211d 100644 --- a/esphome/components/graphical_layout/horizontal_stack.py +++ b/esphome/components/graphical_layout/horizontal_stack.py @@ -4,10 +4,19 @@ from esphome.const import CONF_ID, CONF_TYPE graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout") HorizontalStack = graphical_layout_ns.class_("HorizontalStack") +VerticalChildAlign = graphical_layout_ns.enum("VerticalChildAlign", is_class=True) CONF_ITEM_PADDING = "item_padding" CONF_HORIZONTAL_STACK = "horizontal_stack" CONF_ITEMS = "items" +CONF_CHILD_ALIGN = "child_align" + +VERTICAL_CHILD_ALIGN = { + "TOP": VerticalChildAlign.TOP, + "CENTER_VERTICAL": VerticalChildAlign.CENTER_VERTICAL, + "BOTTOM": VerticalChildAlign.BOTTOM, + "STRETCH_TO_FIT_HEIGHT": VerticalChildAlign.STRETCH_TO_FIT_HEIGHT, +} def get_config_schema(base_item_schema, item_type_schema): @@ -18,6 +27,7 @@ def get_config_schema(base_item_schema, item_type_schema): cv.Required(CONF_ITEMS): cv.All( cv.ensure_list(item_type_schema), cv.Length(min=1) ), + cv.Optional(CONF_CHILD_ALIGN): cv.enum(VERTICAL_CHILD_ALIGN, upper=True), } ) @@ -25,10 +35,13 @@ def get_config_schema(base_item_schema, item_type_schema): 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]: + if item_padding_config := item_config.get(CONF_ITEM_PADDING): cg.add(var.set_item_padding(item_padding_config)) - for child_item_config in item_config[CONF_ITEMS]: + if child_align := item_config.get(CONF_CHILD_ALIGN): + cg.add(var.set_child_align(child_align)) + + for child_item_config in item_config.get(CONF_ITEMS): 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]( diff --git a/esphome/components/graphical_layout/layout_item.h b/esphome/components/graphical_layout/layout_item.h index 3cdd49c3fc..c16f5f0131 100644 --- a/esphome/components/graphical_layout/layout_item.h +++ b/esphome/components/graphical_layout/layout_item.h @@ -10,6 +10,36 @@ class Rect; namespace graphical_layout { +/* HorizontalChildAlign is used to control alignment of children horizontally */ +enum class HorizontalChildAlign { + /* Aligns all children to the left of their available width */ + LEFT = 0x00, + + /* Aligns all children to the center of the available width */ + CENTER_HORIZONTAL = 0x01, + + /* Aligns all children to the right of the available width */ + RIGHT = 0x02, + + /* Regardless of the requested size of a child they will be given the entire width of their parent */ + STRETCH_TO_FIT_WIDTH = 0x03 +}; + +/* VerticalChildAlign is used to control alignment of children vertically */ +enum class VerticalChildAlign { + /* Aligns all children to the top of the available height */ + TOP = 0x00, + + /* Aligns all children with the center of the available height */ + CENTER_VERTICAL = 0x01, + + /* Aligns all children to the bottom of the available height*/ + BOTTOM = 0x02, + + /* Regardless of the requested size of a child they will be given the entire height of their parent */ + STRETCH_TO_FIT_HEIGHT = 0x03 +}; + /** LayoutItem is the base from which all items derive from*/ class LayoutItem { public: diff --git a/esphome/components/graphical_layout/vertical_stack.cpp b/esphome/components/graphical_layout/vertical_stack.cpp index bb357c68cf..ec6acc0896 100644 --- a/esphome/components/graphical_layout/vertical_stack.cpp +++ b/esphome/components/graphical_layout/vertical_stack.cpp @@ -11,6 +11,7 @@ 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_); + ESP_LOGCONFIG(TAG, "%*sChild alignment: %i", indent_depth, "", (int)this->child_align_); ESP_LOGCONFIG(TAG, "%*sChildren: %i", indent_depth, "", this->children_.size()); for (LayoutItem *child : this->children_) { @@ -38,9 +39,38 @@ void VerticalStack::render_internal(display::Display *display, display::Rect bou for (LayoutItem *item : this->children_) { display::Rect measure = item->measure_item(display); + bool align_altered_local_coordinates = false; + + switch (this->child_align_) { + case HorizontalChildAlign::CENTER_HORIZONTAL: { + align_altered_local_coordinates = true; + int adjustment = (bounds.w - measure.w) / 2; + display->set_local_coordinates_relative_to_current(adjustment, 0); + break; + } + case HorizontalChildAlign::RIGHT: { + align_altered_local_coordinates = true; + display->set_local_coordinates_relative_to_current(bounds.w - measure.w, 0); + break; + } + case HorizontalChildAlign::STRETCH_TO_FIT_WIDTH: { + // Items always get the same width as the widest item + measure.w = bounds.w; + break; + } + case HorizontalChildAlign::LEFT: + default: { + // No action + break; + } + } display->set_local_coordinates_relative_to_current(this->item_padding_, height_offset); item->render(display, measure); + if (align_altered_local_coordinates) { + // Additional pop of local coords due to alignment + display->pop_local_coordinates(); + } display->pop_local_coordinates(); height_offset += measure.h + this->item_padding_; } diff --git a/esphome/components/graphical_layout/vertical_stack.h b/esphome/components/graphical_layout/vertical_stack.h index 2c21f539a9..3cfe44f053 100644 --- a/esphome/components/graphical_layout/vertical_stack.h +++ b/esphome/components/graphical_layout/vertical_stack.h @@ -15,9 +15,11 @@ class VerticalStack : public ContainerLayoutItem { void dump_config(int indent_depth, int additional_level_depth) override; void set_item_padding(int item_padding) { this->item_padding_ = item_padding; }; + void set_child_align(HorizontalChildAlign child_align) { this->child_align_ = child_align; }; protected: int item_padding_{0}; + HorizontalChildAlign child_align_{HorizontalChildAlign::LEFT}; }; } // namespace graphical_layout diff --git a/esphome/components/graphical_layout/vertical_stack.py b/esphome/components/graphical_layout/vertical_stack.py index 9497819aba..c9fde899be 100644 --- a/esphome/components/graphical_layout/vertical_stack.py +++ b/esphome/components/graphical_layout/vertical_stack.py @@ -4,11 +4,19 @@ from esphome.const import CONF_ID, CONF_TYPE graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout") VerticalStack = graphical_layout_ns.class_("VerticalStack") +HorizontalChildAlign = graphical_layout_ns.enum("HorizontalChildAlign", is_class=True) CONF_ITEM_PADDING = "item_padding" CONF_VERTICAL_STACK = "vertical_stack" CONF_ITEMS = "items" +CONF_CHILD_ALIGN = "child_align" +HORIZONTAL_CHILD_ALIGN = { + "LEFT": HorizontalChildAlign.LEFT, + "CENTER_HORIZONTAL": HorizontalChildAlign.CENTER_HORIZONTAL, + "RIGHT": HorizontalChildAlign.RIGHT, + "STRETCH_TO_FIT_WIDTH": HorizontalChildAlign.STRETCH_TO_FIT_WIDTH, +} def get_config_schema(base_item_schema, item_type_schema): return base_item_schema.extend( @@ -18,6 +26,7 @@ def get_config_schema(base_item_schema, item_type_schema): cv.Required(CONF_ITEMS): cv.All( cv.ensure_list(item_type_schema), cv.Length(min=1) ), + cv.Optional(CONF_CHILD_ALIGN): cv.enum(HORIZONTAL_CHILD_ALIGN, upper=True), } ) @@ -28,6 +37,9 @@ async def config_to_layout_item(pvariable_builder, item_config, child_item_build if item_padding_config := item_config[CONF_ITEM_PADDING]: cg.add(var.set_item_padding(item_padding_config)) + if child_align := item_config.get(CONF_CHILD_ALIGN): + cg.add(var.set_child_align(child_align)) + for child_item_config in item_config[CONF_ITEMS]: child_item_type = child_item_config[CONF_TYPE] if child_item_type in child_item_builder: