Add support for margin, padding, and border on the base LayoutItem

Margin is the space between items
Border is a an outline of the element
Padding is the space between the border and the actual drawing
This is supported by a measure/render pair that are implemented in LayoutItem which handle margin/border/padding this calls measure_internal/render_internal
Most implementations will simply override the _internal versions and make use of the existing drawing/calculations
This commit is contained in:
Michael Davidson 2023-12-23 10:55:36 +11:00
parent d6c9a8133e
commit 6574ca68a2
No known key found for this signature in database
GPG key ID: B8D1A99712B8B0EB
15 changed files with 161 additions and 35 deletions

View file

@ -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}"

View file

@ -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

View file

@ -16,8 +16,8 @@ using display_writer_t = std::function<void(display::Display &)>;
*/
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; };

View file

@ -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))

View file

@ -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_) {

View file

@ -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; };

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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());
}

View file

@ -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; };

View file

@ -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))

View file

@ -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_) {

View file

@ -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; };

View file

@ -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: