Add support for controlling child alignment in Horizontal and Vertical Stack

This allows the user to left/center/right/stretch items in the VerticalStack and top/center/bototm/stretch items in the HorizontalStack
This commit is contained in:
Michael Davidson 2023-12-26 13:00:19 +11:00
parent 6574ca68a2
commit 631a969107
No known key found for this signature in database
GPG key ID: B8D1A99712B8B0EB
7 changed files with 122 additions and 2 deletions

View file

@ -11,6 +11,7 @@ static const char *const TAG = "horizontalstack";
void HorizontalStack::dump_config(int indent_depth, int additional_level_depth) { 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, "%*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()); ESP_LOGCONFIG(TAG, "%*sChildren: %i", indent_depth, "", this->children_.size());
for (LayoutItem *child : this->children_) { for (LayoutItem *child : this->children_) {
@ -40,8 +41,38 @@ void HorizontalStack::render_internal(display::Display *display, display::Rect b
for (LayoutItem *item : this->children_) { for (LayoutItem *item : this->children_) {
display::Rect measure = item->measure_item(display); 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_); display->set_local_coordinates_relative_to_current(width_offset, this->item_padding_);
item->render(display, measure); 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(); display->pop_local_coordinates();
width_offset += measure.w + this->item_padding_; width_offset += measure.w + this->item_padding_;
} }

View file

@ -16,9 +16,11 @@ class HorizontalStack : public ContainerLayoutItem {
void dump_config(int indent_depth, int additional_level_depth) override; 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_item_padding(int item_padding) { this->item_padding_ = item_padding; };
void set_child_align(VerticalChildAlign child_align) { this->child_align_ = child_align; };
protected: protected:
int item_padding_{0}; int item_padding_{0};
VerticalChildAlign child_align_{VerticalChildAlign::TOP};
}; };
} // namespace graphical_layout } // namespace graphical_layout

View file

@ -4,10 +4,19 @@ from esphome.const import CONF_ID, CONF_TYPE
graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout") graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout")
HorizontalStack = graphical_layout_ns.class_("HorizontalStack") HorizontalStack = graphical_layout_ns.class_("HorizontalStack")
VerticalChildAlign = graphical_layout_ns.enum("VerticalChildAlign", is_class=True)
CONF_ITEM_PADDING = "item_padding" CONF_ITEM_PADDING = "item_padding"
CONF_HORIZONTAL_STACK = "horizontal_stack" CONF_HORIZONTAL_STACK = "horizontal_stack"
CONF_ITEMS = "items" 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): 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.Required(CONF_ITEMS): cv.All(
cv.ensure_list(item_type_schema), cv.Length(min=1) 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): async def config_to_layout_item(pvariable_builder, item_config, child_item_builder):
var = await pvariable_builder(item_config) 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)) 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] child_item_type = child_item_config[CONF_TYPE]
if child_item_type in child_item_builder: if child_item_type in child_item_builder:
child_item_var = await child_item_builder[child_item_type]( child_item_var = await child_item_builder[child_item_type](

View file

@ -10,6 +10,36 @@ class Rect;
namespace graphical_layout { 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*/ /** LayoutItem is the base from which all items derive from*/
class LayoutItem { class LayoutItem {
public: public:

View file

@ -11,6 +11,7 @@ static const char *const TAG = "verticalstack";
void VerticalStack::dump_config(int indent_depth, int additional_level_depth) { 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, "%*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()); ESP_LOGCONFIG(TAG, "%*sChildren: %i", indent_depth, "", this->children_.size());
for (LayoutItem *child : this->children_) { for (LayoutItem *child : this->children_) {
@ -38,9 +39,38 @@ void VerticalStack::render_internal(display::Display *display, display::Rect bou
for (LayoutItem *item : this->children_) { for (LayoutItem *item : this->children_) {
display::Rect measure = item->measure_item(display); 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); display->set_local_coordinates_relative_to_current(this->item_padding_, height_offset);
item->render(display, measure); 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(); display->pop_local_coordinates();
height_offset += measure.h + this->item_padding_; height_offset += measure.h + this->item_padding_;
} }

View file

@ -15,9 +15,11 @@ class VerticalStack : public ContainerLayoutItem {
void dump_config(int indent_depth, int additional_level_depth) override; 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_item_padding(int item_padding) { this->item_padding_ = item_padding; };
void set_child_align(HorizontalChildAlign child_align) { this->child_align_ = child_align; };
protected: protected:
int item_padding_{0}; int item_padding_{0};
HorizontalChildAlign child_align_{HorizontalChildAlign::LEFT};
}; };
} // namespace graphical_layout } // namespace graphical_layout

View file

@ -4,11 +4,19 @@ from esphome.const import CONF_ID, CONF_TYPE
graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout") graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout")
VerticalStack = graphical_layout_ns.class_("VerticalStack") VerticalStack = graphical_layout_ns.class_("VerticalStack")
HorizontalChildAlign = graphical_layout_ns.enum("HorizontalChildAlign", is_class=True)
CONF_ITEM_PADDING = "item_padding" CONF_ITEM_PADDING = "item_padding"
CONF_VERTICAL_STACK = "vertical_stack" CONF_VERTICAL_STACK = "vertical_stack"
CONF_ITEMS = "items" 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): def get_config_schema(base_item_schema, item_type_schema):
return base_item_schema.extend( 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.Required(CONF_ITEMS): cv.All(
cv.ensure_list(item_type_schema), cv.Length(min=1) 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]: if item_padding_config := item_config[CONF_ITEM_PADDING]:
cg.add(var.set_item_padding(item_padding_config)) 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]: for child_item_config in item_config[CONF_ITEMS]:
child_item_type = child_item_config[CONF_TYPE] child_item_type = child_item_config[CONF_TYPE]
if child_item_type in child_item_builder: if child_item_type in child_item_builder: