mirror of
https://github.com/esphome/esphome.git
synced 2024-11-27 17:27:59 +01:00
Initial check in of a graphical layout system
WIP: But supports vertical and horizontal stacking of items and a simple text item
This commit is contained in:
parent
313cb2bff5
commit
399bbe29e8
19 changed files with 587 additions and 0 deletions
|
@ -116,6 +116,7 @@ esphome/components/gp8403/* @jesserockz
|
||||||
esphome/components/gpio/* @esphome/core
|
esphome/components/gpio/* @esphome/core
|
||||||
esphome/components/gps/* @coogle
|
esphome/components/gps/* @coogle
|
||||||
esphome/components/graph/* @synco
|
esphome/components/graph/* @synco
|
||||||
|
esphome/components/graphical_layout/* @MrMDavidson
|
||||||
esphome/components/gree/* @orestismers
|
esphome/components/gree/* @orestismers
|
||||||
esphome/components/grove_tb6612fng/* @max246
|
esphome/components/grove_tb6612fng/* @max246
|
||||||
esphome/components/growatt_solar/* @leeuwte
|
esphome/components/growatt_solar/* @leeuwte
|
||||||
|
|
|
@ -166,6 +166,13 @@ void Display::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, in
|
||||||
}
|
}
|
||||||
#endif // USE_QR_CODE
|
#endif // USE_QR_CODE
|
||||||
|
|
||||||
|
#ifdef USE_GRAPHICAL_LAYOUT
|
||||||
|
void Display::render_layout(int x, int y, graphical_layout::RootLayoutComponent *layout) {
|
||||||
|
display::Rect b2(x, y, 100, 100);
|
||||||
|
layout->render_at(this, x, y);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1,
|
void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1,
|
||||||
int *width, int *height) {
|
int *width, int *height) {
|
||||||
int x_offset, baseline;
|
int x_offset, baseline;
|
||||||
|
|
|
@ -18,6 +18,10 @@
|
||||||
#include "esphome/components/qr_code/qr_code.h"
|
#include "esphome/components/qr_code/qr_code.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_GRAPHICAL_LAYOUT
|
||||||
|
#include "esphome/components/graphical_layout/graphical_layout.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace display {
|
namespace display {
|
||||||
|
|
||||||
|
@ -393,6 +397,17 @@ class Display : public PollingComponent {
|
||||||
void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1);
|
void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_GRAPHICAL_LAYOUT
|
||||||
|
/** Draw the graphical layout with the top corner at [x,y]
|
||||||
|
*
|
||||||
|
* @param x The x coordinate of the upper left corner
|
||||||
|
* @param y The y coordinate of the upper left corner
|
||||||
|
* @param layout The graphical layout to render
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
void render_layout(int x, int y, graphical_layout::RootLayoutComponent *layout);
|
||||||
|
#endif
|
||||||
|
|
||||||
/** Get the text bounds of the given string.
|
/** Get the text bounds of the given string.
|
||||||
*
|
*
|
||||||
* @param x The x coordinate to place the string at, can be 0 if only interested in dimensions.
|
* @param x The x coordinate to place the string at, can be 0 if only interested in dimensions.
|
||||||
|
|
91
esphome/components/graphical_layout/__init__.py
Normal file
91
esphome/components/graphical_layout/__init__.py
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
import esphome.components.graphical_layout.horizontal_stack as horizontal_stack
|
||||||
|
import esphome.components.graphical_layout.vertical_stack as vertical_stack
|
||||||
|
import esphome.components.graphical_layout.text_panel as text_panel
|
||||||
|
from esphome.components import font, color
|
||||||
|
from esphome.const import CONF_ID
|
||||||
|
|
||||||
|
graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout")
|
||||||
|
RootLayoutComponent = graphical_layout_ns.class_("RootLayoutComponent", cg.Component)
|
||||||
|
LayoutItem = graphical_layout_ns.class_("LayoutItem")
|
||||||
|
ContainerLayoutItem = graphical_layout_ns.class_("ContainerLayoutItem", LayoutItem)
|
||||||
|
|
||||||
|
CODEOWNERS = ["@MrMDavidson"]
|
||||||
|
|
||||||
|
AUTO_LOAD = ["display"]
|
||||||
|
|
||||||
|
MULTI_CONF = True
|
||||||
|
|
||||||
|
CONF_ITEMS = "items"
|
||||||
|
CONF_LAYOUT = "layout"
|
||||||
|
CONF_ITEM_TYPE = "type"
|
||||||
|
|
||||||
|
BASE_ITEM_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def item_type_schema(value):
|
||||||
|
return ITEM_TYPE_SCHEMA(value)
|
||||||
|
|
||||||
|
ITEM_TYPE_SCHEMA = cv.typed_schema(
|
||||||
|
{
|
||||||
|
text_panel.CONF_TYPE: cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(text_panel.TextPanel),
|
||||||
|
cv.Optional(text_panel.CONF_ITEM_PADDING, default=0): cv.templatable(cv.int_),
|
||||||
|
cv.Required(text_panel.CONF_FONT): cv.use_id(font.Font),
|
||||||
|
cv.Optional(text_panel.CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct),
|
||||||
|
cv.Optional(text_panel.CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct),
|
||||||
|
cv.Required(text_panel.CONF_TEXT): cv.templatable(cv.string),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
horizontal_stack.CONF_TYPE: BASE_ITEM_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(horizontal_stack.HorizontalStack),
|
||||||
|
cv.Optional(horizontal_stack.CONF_ITEM_PADDING, default=0): cv.templatable(cv.int_),
|
||||||
|
cv.Required(CONF_ITEMS): cv.All(
|
||||||
|
cv.ensure_list(item_type_schema), cv.Length(min=1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
vertical_stack.CONF_TYPE: BASE_ITEM_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(vertical_stack.VerticalStack),
|
||||||
|
cv.Optional(vertical_stack.CONF_ITEM_PADDING, default=0): cv.templatable(cv.int_),
|
||||||
|
cv.Required(CONF_ITEMS): cv.All(
|
||||||
|
cv.ensure_list(item_type_schema), cv.Length(min=1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
CODE_GENERATORS = {
|
||||||
|
text_panel.CONF_TYPE: text_panel.config_to_layout_item,
|
||||||
|
horizontal_stack.CONF_TYPE: horizontal_stack.config_to_layout_item,
|
||||||
|
vertical_stack.CONF_TYPE: vertical_stack.config_to_layout_item
|
||||||
|
}
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(RootLayoutComponent),
|
||||||
|
cv.Required(CONF_LAYOUT): ITEM_TYPE_SCHEMA
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
|
layout_config = config[CONF_LAYOUT]
|
||||||
|
layout_type = layout_config[CONF_ITEM_TYPE]
|
||||||
|
if layout_type in CODE_GENERATORS:
|
||||||
|
layout_var = await CODE_GENERATORS[layout_type](layout_config, CODE_GENERATORS)
|
||||||
|
cg.add(var.set_layout_root(layout_var))
|
||||||
|
else:
|
||||||
|
raise f"Do not know how to build type {layout_type}"
|
||||||
|
|
||||||
|
cg.add_define("USE_GRAPHICAL_LAYOUT")
|
29
esphome/components/graphical_layout/container_layout_item.h
Normal file
29
esphome/components/graphical_layout/container_layout_item.h
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/graphical_layout/layout_item.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace display {
|
||||||
|
class Display;
|
||||||
|
class Rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace graphical_layout {
|
||||||
|
|
||||||
|
/** The ContainerLayoutItem can be used to derive from when a layout item has children.
|
||||||
|
* It does not define what or how child items get used just that they exist for the item
|
||||||
|
*/
|
||||||
|
class ContainerLayoutItem : public LayoutItem {
|
||||||
|
public:
|
||||||
|
|
||||||
|
/** Adds an item to this container */
|
||||||
|
void add_item(LayoutItem *child) {
|
||||||
|
this->children_.push_back(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::vector<LayoutItem *> children_;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
37
esphome/components/graphical_layout/graphical_layout.cpp
Normal file
37
esphome/components/graphical_layout/graphical_layout.cpp
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
#include "graphical_layout.h"
|
||||||
|
|
||||||
|
#include "esphome/components/display/display.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace graphical_layout {
|
||||||
|
|
||||||
|
static const char *const TAG = "rootlayoutcomponent";
|
||||||
|
|
||||||
|
void RootLayoutComponent::setup() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void RootLayoutComponent::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "Graphical Layout");
|
||||||
|
this->layout_root_->dump_config(2, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RootLayoutComponent::render_at(display::Display *display, int x, int y) {
|
||||||
|
display->set_local_coordinate(x, y);
|
||||||
|
|
||||||
|
display::Rect layout_rect = this->layout_root_->measure_item(display);
|
||||||
|
display::Rect clipping_rect = display::Rect(x, y, layout_rect.w, layout_rect.h);
|
||||||
|
|
||||||
|
// TODO: Should clipping be relative to local?
|
||||||
|
display->start_clipping(clipping_rect);
|
||||||
|
|
||||||
|
// Render everything
|
||||||
|
this->layout_root_->render(display, layout_rect);
|
||||||
|
|
||||||
|
display->pop_local_coordinates();
|
||||||
|
display->shrink_clipping(clipping_rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace graphical_layout
|
||||||
|
} // namespace esphome
|
38
esphome/components/graphical_layout/graphical_layout.h
Normal file
38
esphome/components/graphical_layout/graphical_layout.h
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/display/rect.h"
|
||||||
|
#include "esphome/components/graphical_layout/layout_item.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace display {
|
||||||
|
class Display;
|
||||||
|
class Rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace graphical_layout {
|
||||||
|
|
||||||
|
/** Component used for rendering the layout*/
|
||||||
|
class RootLayoutComponent : public Component {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
/** Render the graphical layout to the screen
|
||||||
|
*
|
||||||
|
* param[in] display: Display that will be rendered to
|
||||||
|
* param[in] x: x coordinate to render at
|
||||||
|
* param[in] y: y coorindate to render at
|
||||||
|
*/
|
||||||
|
void render_at(display::Display *display, int x, int y);
|
||||||
|
|
||||||
|
void set_layout_root(LayoutItem *layout) { this->layout_root_ = layout; };
|
||||||
|
|
||||||
|
protected:
|
||||||
|
LayoutItem *layout_root_{nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace graphical_layout
|
||||||
|
} // namespace esphome
|
51
esphome/components/graphical_layout/horizontal_stack.cpp
Normal file
51
esphome/components/graphical_layout/horizontal_stack.cpp
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
#include "horizontal_stack.h"
|
||||||
|
|
||||||
|
#include "esphome/components/display/display.h"
|
||||||
|
#include "esphome/components/display/rect.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace graphical_layout {
|
||||||
|
|
||||||
|
static const char*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, "%*sChildren: %i", indent_depth, "", this->children_.size());
|
||||||
|
|
||||||
|
for (LayoutItem *child : this->children_) {
|
||||||
|
child->dump_config(indent_depth + additional_level_depth, additional_level_depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const display::Rect HorizontalStack::measure_item(display::Display *display) {
|
||||||
|
display::Rect rect(this->item_padding_, 0, 0, 0);
|
||||||
|
|
||||||
|
for (LayoutItem *child : this->children_) {
|
||||||
|
display::Rect child_rect = child->measure_item(display);
|
||||||
|
rect.h = std::max(rect.h, child_rect.h);
|
||||||
|
rect.w += child_rect.w + this->item_padding_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add item padding top and bottom
|
||||||
|
rect.h += (this->item_padding_ * 2);
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Measured size is (%i, %i, %i, %i)", rect.x, rect.y, rect.x2(), rect.y2());
|
||||||
|
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HorizontalStack::render(display::Display *display, display::Rect bounds) {
|
||||||
|
int width_offset = this->item_padding_;
|
||||||
|
|
||||||
|
for (LayoutItem *item : this->children_) {
|
||||||
|
display::Rect measure = item->measure_item(display);
|
||||||
|
display->set_local_coordinates_relative_to_current(width_offset, this->item_padding_);
|
||||||
|
item->render(display, measure);
|
||||||
|
display->pop_local_coordinates();
|
||||||
|
width_offset += measure.w + this->item_padding_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
25
esphome/components/graphical_layout/horizontal_stack.h
Normal file
25
esphome/components/graphical_layout/horizontal_stack.h
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/graphical_layout/graphical_layout.h"
|
||||||
|
#include "esphome/components/graphical_layout/container_layout_item.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace graphical_layout {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HorizontalStack is a UI element which will render a series of items left-to-right across a display
|
||||||
|
*/
|
||||||
|
class HorizontalStack : public ContainerLayoutItem {
|
||||||
|
public:
|
||||||
|
const display::Rect measure_item(display::Display *display);
|
||||||
|
void render(display::Display *display, display::Rect bounds);
|
||||||
|
|
||||||
|
void dump_config(int indent_depth, int additional_level_depth);
|
||||||
|
void set_item_padding(int item_padding) { this->item_padding_ = item_padding; };
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int item_padding_{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
27
esphome/components/graphical_layout/horizontal_stack.py
Normal file
27
esphome/components/graphical_layout/horizontal_stack.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_ID
|
||||||
|
|
||||||
|
graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout")
|
||||||
|
HorizontalStack = graphical_layout_ns.class_("HorizontalStack")
|
||||||
|
|
||||||
|
CONF_ITEM_PADDING = "item_padding"
|
||||||
|
CONF_TYPE = "horizontal_stack"
|
||||||
|
CONF_ITEMS = "items"
|
||||||
|
CONF_ITEM_TYPE = "type"
|
||||||
|
|
||||||
|
async def config_to_layout_item(item_config, child_item_builder):
|
||||||
|
var = cg.new_Pvariable(item_config[CONF_ID])
|
||||||
|
|
||||||
|
if item_padding_config := item_config[CONF_ITEM_PADDING]:
|
||||||
|
cg.add(var.set_item_padding(item_padding_config))
|
||||||
|
|
||||||
|
for child_item_config in item_config[CONF_ITEMS]:
|
||||||
|
child_item_type = child_item_config[CONF_ITEM_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)
|
||||||
|
cg.add(var.add_item(child_item_var))
|
||||||
|
else:
|
||||||
|
raise f"Do not know how to build type {child_item_type}"
|
||||||
|
|
||||||
|
return var
|
37
esphome/components/graphical_layout/layout_item.h
Normal file
37
esphome/components/graphical_layout/layout_item.h
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace display {
|
||||||
|
class Display;
|
||||||
|
class Rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
*
|
||||||
|
* param[in] display: Display that will be used for rendering. May be used to help with calculations
|
||||||
|
*/
|
||||||
|
virtual const display::Rect measure_item(display::Display *display) = 0;
|
||||||
|
|
||||||
|
/** Perform the rendering of the item to the display
|
||||||
|
*
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
9
esphome/components/graphical_layout/layoutexport.py
Normal file
9
esphome/components/graphical_layout/layoutexport.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from typing import Awaitable, Any, Callable, Optional
|
||||||
|
|
||||||
|
class LayoutImport:
|
||||||
|
def __init__(self, name : str, schema_builder_func: Callable[[cv.Schema, cv.Schema, cv.Schema], cv.Schema], builder_func : Awaitable[Any], parent_schema_builder_func : Optional[Callable[[], cv.Schema]] = None):
|
||||||
|
self.name = name
|
||||||
|
self.schema_builder_func = schema_builder_func
|
||||||
|
self.builder_func = builder_func
|
||||||
|
self.parent_schema_builder_func = parent_schema_builder_func
|
32
esphome/components/graphical_layout/text_panel.cpp
Normal file
32
esphome/components/graphical_layout/text_panel.cpp
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
#include "text_panel.h"
|
||||||
|
|
||||||
|
#include "esphome/components/display/display.h"
|
||||||
|
#include "esphome/components/display/rect.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace graphical_layout {
|
||||||
|
|
||||||
|
static const char *const TAG = "textpanel";
|
||||||
|
|
||||||
|
void TextPanel::dump_config(int indent_depth, int additional_level_depth) {
|
||||||
|
ESP_LOGCONFIG(TAG, "%*sText: %s", indent_depth, "", this->text_.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
const display::Rect TextPanel::measure_item(display::Display *display) {
|
||||||
|
int x1;
|
||||||
|
int y1;
|
||||||
|
int width;
|
||||||
|
int height;
|
||||||
|
|
||||||
|
display->get_text_bounds(0, 0, this->text_.c_str(), this->font_, display::TextAlign::TOP_LEFT, &x1, &y1, &width, &height);
|
||||||
|
|
||||||
|
return display::Rect(0, 0, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextPanel::render(display::Display *display, display::Rect bounds) {
|
||||||
|
display->print(0, 0, this->font_, this->foreground_color_, display::TextAlign::TOP_LEFT, this->text_.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
36
esphome/components/graphical_layout/text_panel.h
Normal file
36
esphome/components/graphical_layout/text_panel.h
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/graphical_layout/graphical_layout.h"
|
||||||
|
#include "esphome/components/font/font.h"
|
||||||
|
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace graphical_layout {
|
||||||
|
|
||||||
|
const Color COLOR_ON(255, 255, 255, 255);
|
||||||
|
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:
|
||||||
|
const display::Rect measure_item(display::Display *display);
|
||||||
|
void render(display::Display *display, display::Rect bounds);
|
||||||
|
void dump_config(int indent_depth, int additional_level_depth);
|
||||||
|
|
||||||
|
void set_item_padding(int item_padding) { this->item_padding_ = item_padding; };
|
||||||
|
void set_text(std::string text) { this->text_ = text; };
|
||||||
|
void set_font(display::BaseFont *font) { this->font_ = font; };
|
||||||
|
void set_foreground_color(Color foreground_color) { this->foreground_color_ = foreground_color; };
|
||||||
|
void set_background_color(Color background_color) { this->background_color_ = background_color; };
|
||||||
|
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int item_padding_{0};
|
||||||
|
std::string text_{};
|
||||||
|
display::BaseFont *font_{nullptr};
|
||||||
|
Color foreground_color_{COLOR_ON};
|
||||||
|
Color background_color_{COLOR_OFF};
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
48
esphome/components/graphical_layout/text_panel.py
Normal file
48
esphome/components/graphical_layout/text_panel.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.components import font, color
|
||||||
|
from esphome.const import CONF_ID
|
||||||
|
|
||||||
|
graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout")
|
||||||
|
TextPanel = graphical_layout_ns.class_("TextPanel")
|
||||||
|
|
||||||
|
CONF_ITEM_PADDING = "item_padding"
|
||||||
|
CONF_TYPE = "text_panel"
|
||||||
|
CONF_FONT = "font"
|
||||||
|
CONF_FOREGROUND_COLOR = "foreground_color"
|
||||||
|
CONF_BACKGROUND_COLOR = "background_color"
|
||||||
|
CONF_TEXT = "text"
|
||||||
|
|
||||||
|
LAYOUT_ITEM_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(TextPanel),
|
||||||
|
cv.Optional(CONF_ITEM_PADDING, default=0): cv.templatable(cv.int_),
|
||||||
|
cv.Required(CONF_FONT): cv.use_id(font.Font),
|
||||||
|
cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color.ColorStruct),
|
||||||
|
cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color.ColorStruct),
|
||||||
|
cv.Required(CONF_TEXT): cv.templatable(cv.string),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def config_to_layout_item(item_config, child_item_builder):
|
||||||
|
var = cg.new_Pvariable(item_config[CONF_ID])
|
||||||
|
|
||||||
|
if item_padding_config := item_config[CONF_ITEM_PADDING]:
|
||||||
|
cg.add(var.set_item_padding(item_padding_config))
|
||||||
|
|
||||||
|
font = await cg.get_variable(item_config[CONF_FONT])
|
||||||
|
cg.add(var.set_font(font))
|
||||||
|
|
||||||
|
if foreground_color_config := item_config.get(CONF_FOREGROUND_COLOR):
|
||||||
|
foreground_color = await cg.get_variable(foreground_color_config)
|
||||||
|
cg.add(var.set_foreground_color(foreground_color))
|
||||||
|
|
||||||
|
if background_color_config := item_config.get(CONF_BACKGROUND_COLOR):
|
||||||
|
background_color = await cg.get_variable(background_color_config)
|
||||||
|
cg.add(var.set_background_color(background_color))
|
||||||
|
|
||||||
|
text = await cg.templatable(item_config[CONF_TEXT], args = [], output_type = str)
|
||||||
|
cg.add(var.set_text(text))
|
||||||
|
|
||||||
|
return var
|
50
esphome/components/graphical_layout/vertical_stack.cpp
Normal file
50
esphome/components/graphical_layout/vertical_stack.cpp
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
#include "vertical_stack.h"
|
||||||
|
|
||||||
|
#include "esphome/components/display/display.h"
|
||||||
|
#include "esphome/components/display/rect.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace graphical_layout {
|
||||||
|
|
||||||
|
static const char*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, "%*sChildren: %i", indent_depth, "", this->children_.size());
|
||||||
|
|
||||||
|
for (LayoutItem *child : this->children_) {
|
||||||
|
child->dump_config(indent_depth + additional_level_depth, additional_level_depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const display::Rect VerticalStack::measure_item(display::Display *display) {
|
||||||
|
display::Rect rect(0, this->item_padding_, 0, 0);
|
||||||
|
|
||||||
|
for (LayoutItem *child : this->children_) {
|
||||||
|
display::Rect child_rect = child->measure_item(display);
|
||||||
|
rect.w = std::max(rect.w, child_rect.w);
|
||||||
|
rect.h += child_rect.h + this->item_padding_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add item padding left and right
|
||||||
|
rect.h += (this->item_padding_ * 2);
|
||||||
|
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VerticalStack::render(display::Display *display, display::Rect bounds) {
|
||||||
|
int height_offset = this->item_padding_;
|
||||||
|
|
||||||
|
for (LayoutItem *item : this->children_) {
|
||||||
|
display::Rect measure = item->measure_item(display);
|
||||||
|
|
||||||
|
display->set_local_coordinates_relative_to_current(this->item_padding_, height_offset);
|
||||||
|
item->render(display, measure);
|
||||||
|
display->pop_local_coordinates();
|
||||||
|
height_offset += measure.h + this->item_padding_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
25
esphome/components/graphical_layout/vertical_stack.h
Normal file
25
esphome/components/graphical_layout/vertical_stack.h
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/graphical_layout/graphical_layout.h"
|
||||||
|
#include "esphome/components/graphical_layout/container_layout_item.h"
|
||||||
|
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace graphical_layout {
|
||||||
|
|
||||||
|
/** The HorizontalStack is a UI element which will render a series of items top to bottom down a display
|
||||||
|
*/
|
||||||
|
class VerticalStack : public ContainerLayoutItem {
|
||||||
|
public:
|
||||||
|
const display::Rect measure_item(display::Display *display);
|
||||||
|
void render(display::Display *display, display::Rect bounds);
|
||||||
|
|
||||||
|
void dump_config(int indent_depth, int additional_level_depth);
|
||||||
|
void set_item_padding(int item_padding) { this->item_padding_ = item_padding; };
|
||||||
|
|
||||||
|
protected:
|
||||||
|
int item_padding_{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
28
esphome/components/graphical_layout/vertical_stack.py
Normal file
28
esphome/components/graphical_layout/vertical_stack.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_ID
|
||||||
|
|
||||||
|
graphical_layout_ns = cg.esphome_ns.namespace("graphical_layout")
|
||||||
|
VerticalStack = graphical_layout_ns.class_("VerticalStack")
|
||||||
|
|
||||||
|
CONF_ITEM_PADDING = "item_padding"
|
||||||
|
CONF_TYPE = "vertical_stack"
|
||||||
|
CONF_ITEMS = "items"
|
||||||
|
CONF_ITEM_TYPE = "type"
|
||||||
|
|
||||||
|
|
||||||
|
async def config_to_layout_item(item_config, child_item_builder):
|
||||||
|
var = cg.new_Pvariable(item_config[CONF_ID])
|
||||||
|
|
||||||
|
if item_padding_config := item_config[CONF_ITEM_PADDING]:
|
||||||
|
cg.add(var.set_item_padding(item_padding_config))
|
||||||
|
|
||||||
|
for child_item_config in item_config[CONF_ITEMS]:
|
||||||
|
child_item_type = child_item_config[CONF_ITEM_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)
|
||||||
|
cg.add(var.add_item(child_item_var))
|
||||||
|
else:
|
||||||
|
raise f"Do not know how to build type {child_item_type}"
|
||||||
|
|
||||||
|
return var
|
|
@ -51,6 +51,7 @@
|
||||||
#define USE_UART_DEBUGGER
|
#define USE_UART_DEBUGGER
|
||||||
#define USE_WIFI
|
#define USE_WIFI
|
||||||
#define USE_WIFI_AP
|
#define USE_WIFI_AP
|
||||||
|
#define USE_GRAPHICAL_LAYOUT
|
||||||
|
|
||||||
// Arduino-specific feature flags
|
// Arduino-specific feature flags
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
|
|
Loading…
Reference in a new issue