mirror of
https://github.com/esphome/esphome.git
synced 2024-12-12 08:24:55 +01:00
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:
parent
d6c9a8133e
commit
6574ca68a2
15 changed files with 161 additions and 35 deletions
|
@ -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}"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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; };
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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_) {
|
||||
|
|
|
@ -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; };
|
||||
|
|
|
@ -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:
|
||||
|
|
60
esphome/components/graphical_layout/layout_item.cpp
Normal file
60
esphome/components/graphical_layout/layout_item.cpp
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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; };
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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_) {
|
||||
|
|
|
@ -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; };
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue