mirror of
https://github.com/esphome/esphome.git
synced 2025-11-18 15:55:46 +00:00
[lvgl] Layout improvements (#10149)
Co-authored-by: clydeps <U5yx99dok9> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -41,10 +41,7 @@ from .lv_validation import lv_bool, lv_images_used
|
||||
from .lvcode import LvContext, LvglComponent, lvgl_static
|
||||
from .schemas import (
|
||||
DISP_BG_SCHEMA,
|
||||
FLEX_OBJ_SCHEMA,
|
||||
FULL_STYLE_SCHEMA,
|
||||
GRID_CELL_SCHEMA,
|
||||
LAYOUT_SCHEMAS,
|
||||
WIDGET_TYPES,
|
||||
any_widget_schema,
|
||||
container_schema,
|
||||
@@ -78,6 +75,7 @@ from .widgets.button import button_spec
|
||||
from .widgets.buttonmatrix import buttonmatrix_spec
|
||||
from .widgets.canvas import canvas_spec
|
||||
from .widgets.checkbox import checkbox_spec
|
||||
from .widgets.container import container_spec
|
||||
from .widgets.dropdown import dropdown_spec
|
||||
from .widgets.img import img_spec
|
||||
from .widgets.keyboard import keyboard_spec
|
||||
@@ -130,20 +128,10 @@ for w_type in (
|
||||
tileview_spec,
|
||||
qr_code_spec,
|
||||
canvas_spec,
|
||||
container_spec,
|
||||
):
|
||||
WIDGET_TYPES[w_type.name] = w_type
|
||||
|
||||
WIDGET_SCHEMA = any_widget_schema()
|
||||
|
||||
LAYOUT_SCHEMAS[df.TYPE_GRID] = {
|
||||
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(GRID_CELL_SCHEMA))
|
||||
}
|
||||
LAYOUT_SCHEMAS[df.TYPE_FLEX] = {
|
||||
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema(FLEX_OBJ_SCHEMA))
|
||||
}
|
||||
LAYOUT_SCHEMAS[df.TYPE_NONE] = {
|
||||
cv.Optional(df.CONF_WIDGETS): cv.ensure_list(any_widget_schema())
|
||||
}
|
||||
for w_type in WIDGET_TYPES.values():
|
||||
register_action(
|
||||
f"lvgl.{w_type.name}.update",
|
||||
@@ -410,7 +398,7 @@ def display_schema(config):
|
||||
def add_hello_world(config):
|
||||
if df.CONF_WIDGETS not in config and CONF_PAGES not in config:
|
||||
LOGGER.info("No pages or widgets configured, creating default hello_world page")
|
||||
config[df.CONF_WIDGETS] = cv.ensure_list(WIDGET_SCHEMA)(get_hello_world())
|
||||
config[df.CONF_WIDGETS] = any_widget_schema()(get_hello_world())
|
||||
return config
|
||||
|
||||
|
||||
@@ -450,6 +438,7 @@ LVGL_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_PAGES): cv.ensure_list(container_schema(page_spec)),
|
||||
**{
|
||||
cv.Optional(x): validate_automation(
|
||||
{
|
||||
@@ -459,12 +448,6 @@ LVGL_SCHEMA = cv.All(
|
||||
)
|
||||
for x in SIMPLE_TRIGGERS
|
||||
},
|
||||
cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list(
|
||||
WIDGET_SCHEMA
|
||||
),
|
||||
cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list(
|
||||
container_schema(page_spec)
|
||||
),
|
||||
cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA),
|
||||
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
|
||||
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),
|
||||
|
||||
@@ -394,6 +394,8 @@ LV_FLEX_ALIGNMENTS = LvConstant(
|
||||
"SPACE_BETWEEN",
|
||||
)
|
||||
|
||||
LV_FLEX_CROSS_ALIGNMENTS = LV_FLEX_ALIGNMENTS.extend("STRETCH")
|
||||
|
||||
LV_MENU_MODES = LvConstant(
|
||||
"LV_MENU_HEADER_",
|
||||
"TOP_FIXED",
|
||||
@@ -436,6 +438,7 @@ CONF_BUTTONS = "buttons"
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
CONF_CHANGE_RATE = "change_rate"
|
||||
CONF_CLOSE_BUTTON = "close_button"
|
||||
CONF_CONTAINER = "container"
|
||||
CONF_CONTROL = "control"
|
||||
CONF_DEFAULT_FONT = "default_font"
|
||||
CONF_DEFAULT_GROUP = "default_group"
|
||||
|
||||
357
esphome/components/lvgl/layout.py
Normal file
357
esphome/components/lvgl/layout.py
Normal file
@@ -0,0 +1,357 @@
|
||||
import re
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_HEIGHT, CONF_TYPE, CONF_WIDTH
|
||||
|
||||
from .defines import (
|
||||
CONF_FLEX_ALIGN_CROSS,
|
||||
CONF_FLEX_ALIGN_MAIN,
|
||||
CONF_FLEX_ALIGN_TRACK,
|
||||
CONF_FLEX_FLOW,
|
||||
CONF_FLEX_GROW,
|
||||
CONF_GRID_CELL_COLUMN_POS,
|
||||
CONF_GRID_CELL_COLUMN_SPAN,
|
||||
CONF_GRID_CELL_ROW_POS,
|
||||
CONF_GRID_CELL_ROW_SPAN,
|
||||
CONF_GRID_CELL_X_ALIGN,
|
||||
CONF_GRID_CELL_Y_ALIGN,
|
||||
CONF_GRID_COLUMN_ALIGN,
|
||||
CONF_GRID_COLUMNS,
|
||||
CONF_GRID_ROW_ALIGN,
|
||||
CONF_GRID_ROWS,
|
||||
CONF_LAYOUT,
|
||||
CONF_PAD_COLUMN,
|
||||
CONF_PAD_ROW,
|
||||
CONF_WIDGETS,
|
||||
FLEX_FLOWS,
|
||||
LV_CELL_ALIGNMENTS,
|
||||
LV_FLEX_ALIGNMENTS,
|
||||
LV_FLEX_CROSS_ALIGNMENTS,
|
||||
LV_GRID_ALIGNMENTS,
|
||||
TYPE_FLEX,
|
||||
TYPE_GRID,
|
||||
TYPE_NONE,
|
||||
LvConstant,
|
||||
)
|
||||
from .lv_validation import padding, size
|
||||
|
||||
cell_alignments = LV_CELL_ALIGNMENTS.one_of
|
||||
grid_alignments = LV_GRID_ALIGNMENTS.one_of
|
||||
flex_alignments = LV_FLEX_ALIGNMENTS.one_of
|
||||
|
||||
FLEX_LAYOUT_SCHEMA = {
|
||||
cv.Required(CONF_TYPE): cv.one_of(TYPE_FLEX, lower=True),
|
||||
cv.Optional(CONF_FLEX_FLOW, default="row_wrap"): FLEX_FLOWS.one_of,
|
||||
cv.Optional(CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments,
|
||||
cv.Optional(
|
||||
CONF_FLEX_ALIGN_CROSS, default="start"
|
||||
): LV_FLEX_CROSS_ALIGNMENTS.one_of,
|
||||
cv.Optional(CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments,
|
||||
cv.Optional(CONF_PAD_ROW): padding,
|
||||
cv.Optional(CONF_PAD_COLUMN): padding,
|
||||
cv.Optional(CONF_FLEX_GROW): cv.int_,
|
||||
}
|
||||
|
||||
FLEX_HV_STYLE = {
|
||||
CONF_FLEX_ALIGN_MAIN: "LV_FLEX_ALIGN_SPACE_EVENLY",
|
||||
CONF_FLEX_ALIGN_TRACK: "LV_FLEX_ALIGN_CENTER",
|
||||
CONF_FLEX_ALIGN_CROSS: "LV_FLEX_ALIGN_CENTER",
|
||||
CONF_TYPE: TYPE_FLEX,
|
||||
}
|
||||
|
||||
FLEX_OBJ_SCHEMA = {
|
||||
cv.Optional(CONF_FLEX_GROW): cv.int_,
|
||||
}
|
||||
|
||||
|
||||
def flex_hv_schema(dir):
|
||||
dir = CONF_HEIGHT if dir == "horizontal" else CONF_WIDTH
|
||||
return {
|
||||
cv.Optional(CONF_FLEX_GROW, default=1): cv.int_,
|
||||
cv.Optional(dir, default="100%"): size,
|
||||
}
|
||||
|
||||
|
||||
def grid_free_space(value):
|
||||
value = cv.Upper(value)
|
||||
if value.startswith("FR(") and value.endswith(")"):
|
||||
value = value.removesuffix(")").removeprefix("FR(")
|
||||
return f"LV_GRID_FR({cv.positive_int(value)})"
|
||||
raise cv.Invalid("must be a size in pixels, CONTENT or FR(nn)")
|
||||
|
||||
|
||||
grid_spec = cv.Any(size, LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space)
|
||||
|
||||
GRID_CELL_SCHEMA = {
|
||||
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments,
|
||||
}
|
||||
|
||||
|
||||
class Layout:
|
||||
"""
|
||||
Define properties for a layout
|
||||
The base class is layout "none"
|
||||
"""
|
||||
|
||||
def get_type(self):
|
||||
return TYPE_NONE
|
||||
|
||||
def get_layout_schemas(self, config: dict) -> tuple:
|
||||
"""
|
||||
Get the layout and child schema for a given widget based on its layout type.
|
||||
"""
|
||||
return None, {}
|
||||
|
||||
def validate(self, config):
|
||||
"""
|
||||
Validate the layout configuration. This is called late in the schema validation
|
||||
:param config: The input configuration
|
||||
:return: The validated configuration
|
||||
"""
|
||||
return config
|
||||
|
||||
|
||||
class FlexLayout(Layout):
|
||||
def get_type(self):
|
||||
return TYPE_FLEX
|
||||
|
||||
def get_layout_schemas(self, config: dict) -> tuple:
|
||||
layout = config.get(CONF_LAYOUT)
|
||||
if not isinstance(layout, dict) or layout.get(CONF_TYPE) != TYPE_FLEX:
|
||||
return None, {}
|
||||
child_schema = FLEX_OBJ_SCHEMA
|
||||
if grow := layout.get(CONF_FLEX_GROW):
|
||||
child_schema = {cv.Optional(CONF_FLEX_GROW, default=grow): cv.int_}
|
||||
# Polyfill to implement stretch alignment for flex containers
|
||||
# LVGL does not support this natively, so we add a 100% size property to the children in the cross-axis
|
||||
if layout.get(CONF_FLEX_ALIGN_CROSS) == "LV_FLEX_ALIGN_STRETCH":
|
||||
dimension = (
|
||||
CONF_WIDTH
|
||||
if "COLUMN" in layout[CONF_FLEX_FLOW].upper()
|
||||
else CONF_HEIGHT
|
||||
)
|
||||
child_schema[cv.Optional(dimension, default="100%")] = size
|
||||
return FLEX_LAYOUT_SCHEMA, child_schema
|
||||
|
||||
def validate(self, config):
|
||||
"""
|
||||
Perform validation on the container and its children for this layout
|
||||
:param config:
|
||||
:return:
|
||||
"""
|
||||
return config
|
||||
|
||||
|
||||
class DirectionalLayout(FlexLayout):
|
||||
def __init__(self, direction: str, flow):
|
||||
"""
|
||||
:param direction: "horizontal" or "vertical"
|
||||
:param flow: "row" or "column"
|
||||
"""
|
||||
super().__init__()
|
||||
self.direction = direction
|
||||
self.flow = flow
|
||||
|
||||
def get_type(self):
|
||||
return self.direction
|
||||
|
||||
def get_layout_schemas(self, config: dict) -> tuple:
|
||||
if config.get(CONF_LAYOUT, "").lower() != self.direction:
|
||||
return None, {}
|
||||
return cv.one_of(self.direction, lower=True), flex_hv_schema(self.direction)
|
||||
|
||||
def validate(self, config):
|
||||
assert config[CONF_LAYOUT].lower() == self.direction
|
||||
config[CONF_LAYOUT] = {
|
||||
**FLEX_HV_STYLE,
|
||||
CONF_FLEX_FLOW: "LV_FLEX_FLOW_" + self.flow.upper(),
|
||||
}
|
||||
return config
|
||||
|
||||
|
||||
class GridLayout(Layout):
|
||||
_GRID_LAYOUT_REGEX = re.compile(r"^\s*(\d+)\s*x\s*(\d+)\s*$")
|
||||
|
||||
def get_type(self):
|
||||
return TYPE_GRID
|
||||
|
||||
def get_layout_schemas(self, config: dict) -> tuple:
|
||||
layout = config.get(CONF_LAYOUT)
|
||||
if isinstance(layout, str):
|
||||
if GridLayout._GRID_LAYOUT_REGEX.match(layout):
|
||||
return (
|
||||
cv.string,
|
||||
{
|
||||
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
|
||||
cv.Optional(
|
||||
CONF_GRID_CELL_ROW_SPAN, default=1
|
||||
): cv.positive_int,
|
||||
cv.Optional(
|
||||
CONF_GRID_CELL_COLUMN_SPAN, default=1
|
||||
): cv.positive_int,
|
||||
cv.Optional(
|
||||
CONF_GRID_CELL_X_ALIGN, default="center"
|
||||
): grid_alignments,
|
||||
cv.Optional(
|
||||
CONF_GRID_CELL_Y_ALIGN, default="center"
|
||||
): grid_alignments,
|
||||
},
|
||||
)
|
||||
# Not a valid grid layout string
|
||||
return None, {}
|
||||
|
||||
if not isinstance(layout, dict) or layout.get(CONF_TYPE) != TYPE_GRID:
|
||||
return None, {}
|
||||
return (
|
||||
{
|
||||
cv.Required(CONF_TYPE): cv.one_of(TYPE_GRID, lower=True),
|
||||
cv.Required(CONF_GRID_ROWS): [grid_spec],
|
||||
cv.Required(CONF_GRID_COLUMNS): [grid_spec],
|
||||
cv.Optional(CONF_GRID_COLUMN_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_PAD_ROW): padding,
|
||||
cv.Optional(CONF_PAD_COLUMN): padding,
|
||||
},
|
||||
{
|
||||
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int,
|
||||
cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments,
|
||||
cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments,
|
||||
},
|
||||
)
|
||||
|
||||
def validate(self, config: dict):
|
||||
"""
|
||||
Validate the grid layout.
|
||||
The `layout:` key may be a dictionary with `rows` and `columns` keys, or a string in the format "rows x columns".
|
||||
Either all cells must have a row and column,
|
||||
or none, in which case the grid layout is auto-generated.
|
||||
:param config:
|
||||
:return: The config updated with auto-generated values
|
||||
"""
|
||||
layout = config.get(CONF_LAYOUT)
|
||||
if isinstance(layout, str):
|
||||
# If the layout is a string, assume it is in the format "rows x columns", implying
|
||||
# a grid layout with the specified number of rows and columns each with CONTENT sizing.
|
||||
layout = layout.strip()
|
||||
match = GridLayout._GRID_LAYOUT_REGEX.match(layout)
|
||||
if match:
|
||||
rows = int(match.group(1))
|
||||
cols = int(match.group(2))
|
||||
layout = {
|
||||
CONF_TYPE: TYPE_GRID,
|
||||
CONF_GRID_ROWS: ["LV_GRID_FR(1)"] * rows,
|
||||
CONF_GRID_COLUMNS: ["LV_GRID_FR(1)"] * cols,
|
||||
}
|
||||
config[CONF_LAYOUT] = layout
|
||||
else:
|
||||
raise cv.Invalid(
|
||||
f"Invalid grid layout format: {config}, expected 'rows x columns'",
|
||||
[CONF_LAYOUT],
|
||||
)
|
||||
# should be guaranteed to be a dict at this point
|
||||
assert isinstance(layout, dict)
|
||||
assert layout.get(CONF_TYPE) == TYPE_GRID
|
||||
rows = len(layout[CONF_GRID_ROWS])
|
||||
columns = len(layout[CONF_GRID_COLUMNS])
|
||||
used_cells = [[None] * columns for _ in range(rows)]
|
||||
for index, widget in enumerate(config.get(CONF_WIDGETS, [])):
|
||||
_, w = next(iter(widget.items()))
|
||||
if (CONF_GRID_CELL_COLUMN_POS in w) != (CONF_GRID_CELL_ROW_POS in w):
|
||||
raise cv.Invalid(
|
||||
"Both row and column positions must be specified, or both omitted",
|
||||
[CONF_WIDGETS, index],
|
||||
)
|
||||
if CONF_GRID_CELL_ROW_POS in w:
|
||||
row = w[CONF_GRID_CELL_ROW_POS]
|
||||
column = w[CONF_GRID_CELL_COLUMN_POS]
|
||||
else:
|
||||
try:
|
||||
row, column = next(
|
||||
(r_idx, c_idx)
|
||||
for r_idx, row in enumerate(used_cells)
|
||||
for c_idx, value in enumerate(row)
|
||||
if value is None
|
||||
)
|
||||
except StopIteration:
|
||||
raise cv.Invalid(
|
||||
"No free cells available in grid layout", [CONF_WIDGETS, index]
|
||||
) from None
|
||||
w[CONF_GRID_CELL_ROW_POS] = row
|
||||
w[CONF_GRID_CELL_COLUMN_POS] = column
|
||||
|
||||
for i in range(w[CONF_GRID_CELL_ROW_SPAN]):
|
||||
for j in range(w[CONF_GRID_CELL_COLUMN_SPAN]):
|
||||
if row + i >= rows or column + j >= columns:
|
||||
raise cv.Invalid(
|
||||
f"Cell at {row}/{column} span {w[CONF_GRID_CELL_ROW_SPAN]}x{w[CONF_GRID_CELL_COLUMN_SPAN]} "
|
||||
f"exceeds grid size {rows}x{columns}",
|
||||
[CONF_WIDGETS, index],
|
||||
)
|
||||
if used_cells[row + i][column + j] is not None:
|
||||
raise cv.Invalid(
|
||||
f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}",
|
||||
[CONF_WIDGETS, index],
|
||||
)
|
||||
used_cells[row + i][column + j] = index
|
||||
|
||||
return config
|
||||
|
||||
|
||||
LAYOUT_CLASSES = (
|
||||
FlexLayout(),
|
||||
GridLayout(),
|
||||
DirectionalLayout("horizontal", "row"),
|
||||
DirectionalLayout("vertical", "column"),
|
||||
)
|
||||
LAYOUT_CHOICES = [x.get_type() for x in LAYOUT_CLASSES]
|
||||
|
||||
|
||||
def append_layout_schema(schema, config: dict):
|
||||
"""
|
||||
Get the child layout schema for a given widget based on its layout type.
|
||||
:param config: The config to check
|
||||
:return: A schema for the layout including a widgets key
|
||||
"""
|
||||
# Local import to avoid circular dependencies
|
||||
if CONF_WIDGETS not in config:
|
||||
if CONF_LAYOUT in config:
|
||||
raise cv.Invalid(
|
||||
f"Layout {config[CONF_LAYOUT]} requires a {CONF_WIDGETS} key",
|
||||
[CONF_LAYOUT],
|
||||
)
|
||||
return schema
|
||||
|
||||
from .schemas import any_widget_schema
|
||||
|
||||
if CONF_LAYOUT not in config:
|
||||
# If no layout is specified, return the schema as is
|
||||
return schema.extend({cv.Optional(CONF_WIDGETS): any_widget_schema()})
|
||||
|
||||
for layout_class in LAYOUT_CLASSES:
|
||||
layout_schema, child_schema = layout_class.get_layout_schemas(config)
|
||||
if layout_schema:
|
||||
layout_schema = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_LAYOUT): layout_schema,
|
||||
cv.Required(CONF_WIDGETS): any_widget_schema(child_schema),
|
||||
}
|
||||
)
|
||||
layout_schema.add_extra(layout_class.validate)
|
||||
return layout_schema.extend(schema)
|
||||
|
||||
# If no layout class matched, return a default schema
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_LAYOUT): cv.one_of(*LAYOUT_CHOICES, lower=True),
|
||||
cv.Optional(CONF_WIDGETS): any_widget_schema(),
|
||||
}
|
||||
)
|
||||
@@ -1,3 +1,4 @@
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
@@ -246,6 +247,8 @@ def pixels_or_percent_validator(value):
|
||||
return ["pixels", "..%"]
|
||||
if isinstance(value, str) and value.lower().endswith("px"):
|
||||
value = cv.int_(value[:-2])
|
||||
if isinstance(value, str) and re.match(r"^lv_pct\((\d+)\)$", value):
|
||||
return value
|
||||
value = cv.Any(cv.int_, cv.percentage)(value)
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
|
||||
@@ -299,6 +299,7 @@ class LvExpr(MockLv):
|
||||
|
||||
# Top level mock for generic lv_ calls to be recorded
|
||||
lv = MockLv("lv_")
|
||||
LV = MockLv("LV_")
|
||||
# Just generate an expression
|
||||
lv_expr = LvExpr("lv_")
|
||||
# Mock for lv_obj_ calls
|
||||
@@ -327,7 +328,7 @@ def lv_assign(target, expression):
|
||||
lv_add(AssignmentExpression("", "", target, expression))
|
||||
|
||||
|
||||
def lv_Pvariable(type, name):
|
||||
def lv_Pvariable(type, name) -> MockObj:
|
||||
"""
|
||||
Create but do not initialise a pointer variable
|
||||
:param type: Type of the variable target
|
||||
@@ -343,7 +344,7 @@ def lv_Pvariable(type, name):
|
||||
return var
|
||||
|
||||
|
||||
def lv_variable(type, name):
|
||||
def lv_variable(type, name) -> MockObj:
|
||||
"""
|
||||
Create but do not initialise a variable
|
||||
:param type: Type of the variable target
|
||||
|
||||
@@ -12,17 +12,21 @@ from esphome.const import (
|
||||
CONF_TEXT,
|
||||
CONF_TIME,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE,
|
||||
CONF_X,
|
||||
CONF_Y,
|
||||
)
|
||||
from esphome.core import TimePeriod
|
||||
from esphome.core.config import StartupTrigger
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT
|
||||
|
||||
from . import defines as df, lv_validation as lvalid
|
||||
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR, TYPE_GRID
|
||||
from .helpers import add_lv_use, requires_component, validate_printf
|
||||
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
|
||||
from .helpers import requires_component, validate_printf
|
||||
from .layout import (
|
||||
FLEX_OBJ_SCHEMA,
|
||||
GRID_CELL_SCHEMA,
|
||||
append_layout_schema,
|
||||
grid_alignments,
|
||||
)
|
||||
from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity
|
||||
from .lvcode import LvglComponent, lv_event_t_ptr
|
||||
from .types import (
|
||||
@@ -72,11 +76,9 @@ def _validate_text(value):
|
||||
|
||||
|
||||
# A schema for text properties
|
||||
TEXT_SCHEMA = cv.Schema(
|
||||
{
|
||||
TEXT_SCHEMA = {
|
||||
cv.Optional(CONF_TEXT): _validate_text,
|
||||
}
|
||||
)
|
||||
|
||||
LIST_ACTION_SCHEMA = cv.ensure_list(
|
||||
cv.maybe_simple_value(
|
||||
@@ -136,7 +138,7 @@ STYLE_PROPS = {
|
||||
"arc_opa": lvalid.opacity,
|
||||
"arc_color": lvalid.lv_color,
|
||||
"arc_rounded": lvalid.lv_bool,
|
||||
"arc_width": lvalid.lv_positive_int,
|
||||
"arc_width": lvalid.pixels,
|
||||
"anim_time": lvalid.lv_milliseconds,
|
||||
"bg_color": lvalid.lv_color,
|
||||
"bg_grad": lv_gradient,
|
||||
@@ -223,10 +225,6 @@ STYLE_REMAP = {
|
||||
"image_recolor_opa": "img_recolor_opa",
|
||||
}
|
||||
|
||||
cell_alignments = df.LV_CELL_ALIGNMENTS.one_of
|
||||
grid_alignments = df.LV_GRID_ALIGNMENTS.one_of
|
||||
flex_alignments = df.LV_FLEX_ALIGNMENTS.one_of
|
||||
|
||||
# Complete object style schema
|
||||
STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend(
|
||||
{
|
||||
@@ -266,10 +264,8 @@ def part_schema(parts):
|
||||
:param parts: The parts to include
|
||||
:return: The schema
|
||||
"""
|
||||
return (
|
||||
cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts})
|
||||
.extend(STATE_SCHEMA)
|
||||
.extend(FLAG_SCHEMA)
|
||||
return STATE_SCHEMA.extend(FLAG_SCHEMA).extend(
|
||||
{cv.Optional(part): STATE_SCHEMA for part in parts}
|
||||
)
|
||||
|
||||
|
||||
@@ -277,10 +273,10 @@ def automation_schema(typ: LvType):
|
||||
events = df.LV_EVENT_TRIGGERS + df.SWIPE_TRIGGERS
|
||||
if typ.has_on_value:
|
||||
events = events + (CONF_ON_VALUE,)
|
||||
args = typ.get_arg_type() if isinstance(typ, LvType) else []
|
||||
args = typ.get_arg_type()
|
||||
args.append(lv_event_t_ptr)
|
||||
return cv.Schema(
|
||||
{
|
||||
return {
|
||||
**{
|
||||
cv.Optional(event): validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
@@ -289,14 +285,11 @@ def automation_schema(typ: LvType):
|
||||
}
|
||||
)
|
||||
for event in events
|
||||
}
|
||||
).extend(
|
||||
{
|
||||
},
|
||||
cv.Optional(CONF_ON_BOOT): validate_automation(
|
||||
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger)}
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def base_update_schema(widget_type, parts):
|
||||
@@ -335,75 +328,17 @@ def obj_schema(widget_type: WidgetType):
|
||||
"""
|
||||
return (
|
||||
part_schema(widget_type.parts)
|
||||
.extend(LAYOUT_SCHEMA)
|
||||
.extend(ALIGN_TO_SCHEMA)
|
||||
.extend(automation_schema(widget_type.w_type))
|
||||
.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
|
||||
cv.Optional(CONF_GROUP): cv.use_id(lv_group_t),
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _validate_grid_layout(config):
|
||||
layout = config[df.CONF_LAYOUT]
|
||||
rows = len(layout[df.CONF_GRID_ROWS])
|
||||
columns = len(layout[df.CONF_GRID_COLUMNS])
|
||||
used_cells = [[None] * columns for _ in range(rows)]
|
||||
for index, widget in enumerate(config[df.CONF_WIDGETS]):
|
||||
_, w = next(iter(widget.items()))
|
||||
if (df.CONF_GRID_CELL_COLUMN_POS in w) != (df.CONF_GRID_CELL_ROW_POS in w):
|
||||
# pylint: disable=raise-missing-from
|
||||
raise cv.Invalid(
|
||||
"Both row and column positions must be specified, or both omitted",
|
||||
[df.CONF_WIDGETS, index],
|
||||
)
|
||||
if df.CONF_GRID_CELL_ROW_POS in w:
|
||||
row = w[df.CONF_GRID_CELL_ROW_POS]
|
||||
column = w[df.CONF_GRID_CELL_COLUMN_POS]
|
||||
else:
|
||||
try:
|
||||
row, column = next(
|
||||
(r_idx, c_idx)
|
||||
for r_idx, row in enumerate(used_cells)
|
||||
for c_idx, value in enumerate(row)
|
||||
if value is None
|
||||
)
|
||||
except StopIteration:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise cv.Invalid(
|
||||
"No free cells available in grid layout", [df.CONF_WIDGETS, index]
|
||||
)
|
||||
w[df.CONF_GRID_CELL_ROW_POS] = row
|
||||
w[df.CONF_GRID_CELL_COLUMN_POS] = column
|
||||
|
||||
for i in range(w[df.CONF_GRID_CELL_ROW_SPAN]):
|
||||
for j in range(w[df.CONF_GRID_CELL_COLUMN_SPAN]):
|
||||
if row + i >= rows or column + j >= columns:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise cv.Invalid(
|
||||
f"Cell at {row}/{column} span {w[df.CONF_GRID_CELL_ROW_SPAN]}x{w[df.CONF_GRID_CELL_COLUMN_SPAN]} "
|
||||
f"exceeds grid size {rows}x{columns}",
|
||||
[df.CONF_WIDGETS, index],
|
||||
)
|
||||
if used_cells[row + i][column + j] is not None:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise cv.Invalid(
|
||||
f"Cell span {row + i}/{column + j} already occupied by widget at index {used_cells[row + i][column + j]}",
|
||||
[df.CONF_WIDGETS, index],
|
||||
)
|
||||
used_cells[row + i][column + j] = index
|
||||
|
||||
return config
|
||||
|
||||
|
||||
LAYOUT_SCHEMAS = {}
|
||||
LAYOUT_VALIDATORS = {TYPE_GRID: _validate_grid_layout}
|
||||
|
||||
ALIGN_TO_SCHEMA = {
|
||||
cv.Optional(df.CONF_ALIGN_TO): cv.Schema(
|
||||
{
|
||||
@@ -416,57 +351,6 @@ ALIGN_TO_SCHEMA = {
|
||||
}
|
||||
|
||||
|
||||
def grid_free_space(value):
|
||||
value = cv.Upper(value)
|
||||
if value.startswith("FR(") and value.endswith(")"):
|
||||
value = value.removesuffix(")").removeprefix("FR(")
|
||||
return f"LV_GRID_FR({cv.positive_int(value)})"
|
||||
raise cv.Invalid("must be a size in pixels, CONTENT or FR(nn)")
|
||||
|
||||
|
||||
grid_spec = cv.Any(
|
||||
lvalid.size, df.LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space
|
||||
)
|
||||
|
||||
LAYOUT_SCHEMA = {
|
||||
cv.Optional(df.CONF_LAYOUT): cv.typed_schema(
|
||||
{
|
||||
df.TYPE_GRID: {
|
||||
cv.Required(df.CONF_GRID_ROWS): [grid_spec],
|
||||
cv.Required(df.CONF_GRID_COLUMNS): [grid_spec],
|
||||
cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_PAD_ROW): lvalid.padding,
|
||||
cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding,
|
||||
},
|
||||
df.TYPE_FLEX: {
|
||||
cv.Optional(
|
||||
df.CONF_FLEX_FLOW, default="row_wrap"
|
||||
): df.FLEX_FLOWS.one_of,
|
||||
cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments,
|
||||
cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments,
|
||||
cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments,
|
||||
cv.Optional(df.CONF_PAD_ROW): lvalid.padding,
|
||||
cv.Optional(df.CONF_PAD_COLUMN): lvalid.padding,
|
||||
},
|
||||
},
|
||||
lower=True,
|
||||
)
|
||||
}
|
||||
|
||||
GRID_CELL_SCHEMA = {
|
||||
cv.Optional(df.CONF_GRID_CELL_ROW_POS): cv.positive_int,
|
||||
cv.Optional(df.CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
|
||||
cv.Optional(df.CONF_GRID_CELL_ROW_SPAN, default=1): cv.positive_int,
|
||||
cv.Optional(df.CONF_GRID_CELL_COLUMN_SPAN, default=1): cv.positive_int,
|
||||
cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments,
|
||||
}
|
||||
|
||||
FLEX_OBJ_SCHEMA = {
|
||||
cv.Optional(df.CONF_FLEX_GROW): cv.int_,
|
||||
}
|
||||
|
||||
DISP_BG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any(
|
||||
@@ -498,48 +382,11 @@ ALL_STYLES = {
|
||||
}
|
||||
|
||||
|
||||
def container_validator(schema, widget_type: WidgetType):
|
||||
"""
|
||||
Create a validator for a container given the widget type
|
||||
:param schema: Base schema to extend
|
||||
:param widget_type:
|
||||
:return:
|
||||
"""
|
||||
|
||||
def validator(value):
|
||||
if w_sch := widget_type.schema:
|
||||
if isinstance(w_sch, dict):
|
||||
w_sch = cv.Schema(w_sch)
|
||||
# order is important here to preserve extras
|
||||
result = w_sch.extend(schema)
|
||||
else:
|
||||
result = schema
|
||||
ltype = df.TYPE_NONE
|
||||
if value and (layout := value.get(df.CONF_LAYOUT)):
|
||||
if not isinstance(layout, dict):
|
||||
raise cv.Invalid("Layout value must be a dict")
|
||||
ltype = layout.get(CONF_TYPE)
|
||||
if not ltype:
|
||||
raise (cv.Invalid("Layout schema requires type:"))
|
||||
add_lv_use(ltype)
|
||||
if value == SCHEMA_EXTRACT:
|
||||
return result
|
||||
result = result.extend(
|
||||
LAYOUT_SCHEMAS.get(ltype.lower(), LAYOUT_SCHEMAS[df.TYPE_NONE])
|
||||
)
|
||||
value = result(value)
|
||||
if layout_validator := LAYOUT_VALIDATORS.get(ltype):
|
||||
value = layout_validator(value)
|
||||
return value
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def container_schema(widget_type: WidgetType, extras=None):
|
||||
"""
|
||||
Create a schema for a container widget of a given type. All obj properties are available, plus
|
||||
the extras passed in, plus any defined for the specific widget being specified.
|
||||
:param widget_type: The widget type, e.g. "img"
|
||||
:param widget_type: The widget type, e.g. "image"
|
||||
:param extras: Additional options to be made available, e.g. layout properties for children
|
||||
:return: The schema for this type of widget.
|
||||
"""
|
||||
@@ -549,31 +396,49 @@ def container_schema(widget_type: WidgetType, extras=None):
|
||||
if extras:
|
||||
schema = schema.extend(extras)
|
||||
# Delayed evaluation for recursion
|
||||
return container_validator(schema, widget_type)
|
||||
|
||||
schema = schema.extend(widget_type.schema)
|
||||
|
||||
def widget_schema(widget_type: WidgetType, extras=None):
|
||||
"""
|
||||
Create a schema for a given widget type
|
||||
:param widget_type: The name of the widget
|
||||
:param extras:
|
||||
:return:
|
||||
"""
|
||||
validator = container_schema(widget_type, extras=extras)
|
||||
if required := widget_type.required_component:
|
||||
validator = cv.All(validator, requires_component(required))
|
||||
return cv.Exclusive(widget_type.name, df.CONF_WIDGETS), validator
|
||||
def validator(value):
|
||||
return append_layout_schema(schema, value)(value)
|
||||
|
||||
|
||||
# All widget schemas must be defined before this is called.
|
||||
return validator
|
||||
|
||||
|
||||
def any_widget_schema(extras=None):
|
||||
"""
|
||||
Generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of
|
||||
Dynamically generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of
|
||||
widget under the widgets: key.
|
||||
|
||||
:param extras: Additional schema to be applied to each generated one
|
||||
:return:
|
||||
:return: A validator for the Widgets key
|
||||
"""
|
||||
return cv.Any(dict(widget_schema(wt, extras) for wt in WIDGET_TYPES.values()))
|
||||
|
||||
def validator(value):
|
||||
if isinstance(value, dict):
|
||||
# Convert to list
|
||||
value = [{k: v} for k, v in value.items()]
|
||||
if not isinstance(value, list):
|
||||
raise cv.Invalid("Expected a list of widgets")
|
||||
result = []
|
||||
for index, entry in enumerate(value):
|
||||
if not isinstance(entry, dict) or len(entry) != 1:
|
||||
raise cv.Invalid(
|
||||
"Each widget must be a dictionary with a single key", path=[index]
|
||||
)
|
||||
[(key, value)] = entry.items()
|
||||
# Validate the widget against its schema
|
||||
widget_type = WIDGET_TYPES.get(key)
|
||||
if not widget_type:
|
||||
raise cv.Invalid(f"Unknown widget type: {key}", path=[index])
|
||||
container_validator = container_schema(widget_type, extras=extras)
|
||||
if required := widget_type.required_component:
|
||||
container_validator = cv.All(
|
||||
container_validator, requires_component(required)
|
||||
)
|
||||
# Apply custom validation
|
||||
value = widget_type.validate(value or {})
|
||||
result.append({key: container_validator(value)})
|
||||
return result
|
||||
|
||||
return validator
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import sys
|
||||
|
||||
from esphome import automation, codegen as cg
|
||||
from esphome.config_validation import Schema
|
||||
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.cpp_types import esphome_ns
|
||||
@@ -135,13 +136,13 @@ class WidgetType:
|
||||
self.lv_name = lv_name or name
|
||||
self.w_type = w_type
|
||||
self.parts = parts
|
||||
if schema is None:
|
||||
self.schema = {}
|
||||
else:
|
||||
if not isinstance(schema, Schema):
|
||||
schema = Schema(schema or {})
|
||||
self.schema = schema
|
||||
if modify_schema is None:
|
||||
self.modify_schema = self.schema
|
||||
else:
|
||||
modify_schema = schema
|
||||
if not isinstance(modify_schema, Schema):
|
||||
modify_schema = Schema(modify_schema)
|
||||
self.modify_schema = modify_schema
|
||||
self.mock_obj = MockObj(f"lv_{self.lv_name}", "_")
|
||||
|
||||
@@ -163,7 +164,6 @@ class WidgetType:
|
||||
:param config: Its configuration
|
||||
:return: Generated code as a list of text lines
|
||||
"""
|
||||
return []
|
||||
|
||||
async def obj_creator(self, parent: MockObjClass, config: dict):
|
||||
"""
|
||||
@@ -174,6 +174,13 @@ class WidgetType:
|
||||
"""
|
||||
return lv_expr.call(f"{self.lv_name}_create", parent)
|
||||
|
||||
def on_create(self, var: MockObj, config: dict):
|
||||
"""
|
||||
Called from to_code when the widget is created, to set up any initial properties
|
||||
:param var: The variable representing the widget
|
||||
:param config: Its configuration
|
||||
"""
|
||||
|
||||
def get_uses(self):
|
||||
"""
|
||||
Get a list of other widgets used by this one
|
||||
@@ -193,6 +200,14 @@ class WidgetType:
|
||||
def get_scale(self, config: dict):
|
||||
return 1.0
|
||||
|
||||
def validate(self, value):
|
||||
"""
|
||||
Provides an opportunity for custom validation for a given widget type
|
||||
:param value:
|
||||
:return:
|
||||
"""
|
||||
return value
|
||||
|
||||
|
||||
class NumberType(WidgetType):
|
||||
def get_max(self, config: dict):
|
||||
|
||||
@@ -339,7 +339,10 @@ async def set_obj_properties(w: Widget, config):
|
||||
if layout_type == TYPE_FLEX:
|
||||
lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW]))
|
||||
main = literal(layout[CONF_FLEX_ALIGN_MAIN])
|
||||
cross = literal(layout[CONF_FLEX_ALIGN_CROSS])
|
||||
cross = layout[CONF_FLEX_ALIGN_CROSS]
|
||||
if cross == "LV_FLEX_ALIGN_STRETCH":
|
||||
cross = "LV_FLEX_ALIGN_CENTER"
|
||||
cross = literal(cross)
|
||||
track = literal(layout[CONF_FLEX_ALIGN_TRACK])
|
||||
lv_obj.set_flex_align(w.obj, main, cross, track)
|
||||
parts = collect_parts(config)
|
||||
@@ -446,9 +449,11 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent):
|
||||
if spec.is_compound():
|
||||
var = cg.new_Pvariable(wid)
|
||||
lv_add(var.set_obj(creator))
|
||||
spec.on_create(var.obj, w_cnfig)
|
||||
else:
|
||||
var = lv_Pvariable(lv_obj_t, wid)
|
||||
lv_assign(var, creator)
|
||||
spec.on_create(var, w_cnfig)
|
||||
|
||||
w = Widget.create(wid, var, spec, w_cnfig)
|
||||
if theme := theme_widget_map.get(w_type):
|
||||
|
||||
@@ -159,18 +159,15 @@ async def canvas_set_pixel(config, action_id, template_arg, args):
|
||||
)
|
||||
|
||||
|
||||
DRAW_SCHEMA = cv.Schema(
|
||||
{
|
||||
DRAW_SCHEMA = {
|
||||
cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
|
||||
cv.Required(CONF_X): pixels,
|
||||
cv.Required(CONF_Y): pixels,
|
||||
}
|
||||
)
|
||||
DRAW_OPA_SCHEMA = DRAW_SCHEMA.extend(
|
||||
{
|
||||
DRAW_OPA_SCHEMA = {
|
||||
**DRAW_SCHEMA,
|
||||
cv.Optional(CONF_OPA): opacity,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def draw_to_code(config, dsc_type, props, do_draw, action_id, template_arg, args):
|
||||
@@ -224,12 +221,14 @@ RECT_PROPS = {
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.draw_rectangle",
|
||||
ObjUpdateAction,
|
||||
DRAW_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
**DRAW_OPA_SCHEMA,
|
||||
cv.Required(CONF_WIDTH): cv.templatable(cv.int_),
|
||||
cv.Required(CONF_HEIGHT): cv.templatable(cv.int_),
|
||||
},
|
||||
).extend({cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}),
|
||||
**{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS},
|
||||
}
|
||||
),
|
||||
)
|
||||
async def canvas_draw_rect(config, action_id, template_arg, args):
|
||||
width = await pixels.process(config[CONF_WIDTH])
|
||||
@@ -261,13 +260,14 @@ TEXT_PROPS = {
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.draw_text",
|
||||
ObjUpdateAction,
|
||||
TEXT_SCHEMA.extend(DRAW_OPA_SCHEMA)
|
||||
.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
**TEXT_SCHEMA,
|
||||
**DRAW_OPA_SCHEMA,
|
||||
cv.Required(CONF_MAX_WIDTH): cv.templatable(cv.int_),
|
||||
**{cv.Optional(prop): STYLE_PROPS[f"text_{prop}"] for prop in TEXT_PROPS},
|
||||
},
|
||||
)
|
||||
.extend({cv.Optional(prop): STYLE_PROPS[f"text_{prop}"] for prop in TEXT_PROPS}),
|
||||
),
|
||||
)
|
||||
async def canvas_draw_text(config, action_id, template_arg, args):
|
||||
text = await lv_text.process(config[CONF_TEXT])
|
||||
@@ -293,13 +293,15 @@ IMG_PROPS = {
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.draw_image",
|
||||
ObjUpdateAction,
|
||||
DRAW_OPA_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
**DRAW_OPA_SCHEMA,
|
||||
cv.Required(CONF_SRC): lv_image,
|
||||
cv.Optional(CONF_PIVOT_X, default=0): pixels,
|
||||
cv.Optional(CONF_PIVOT_Y, default=0): pixels,
|
||||
},
|
||||
).extend({cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()}),
|
||||
**{cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()},
|
||||
}
|
||||
),
|
||||
)
|
||||
async def canvas_draw_image(config, action_id, template_arg, args):
|
||||
src = await lv_image.process(config[CONF_SRC])
|
||||
@@ -336,8 +338,9 @@ LINE_PROPS = {
|
||||
cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
|
||||
cv.Optional(CONF_OPA): opacity,
|
||||
cv.Required(CONF_POINTS): cv.ensure_list(point_schema),
|
||||
},
|
||||
).extend({cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()}),
|
||||
**{cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()},
|
||||
}
|
||||
),
|
||||
)
|
||||
async def canvas_draw_line(config, action_id, template_arg, args):
|
||||
points = [
|
||||
@@ -363,8 +366,9 @@ async def canvas_draw_line(config, action_id, template_arg, args):
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
|
||||
cv.Required(CONF_POINTS): cv.ensure_list(point_schema),
|
||||
**{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS},
|
||||
},
|
||||
).extend({cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}),
|
||||
),
|
||||
)
|
||||
async def canvas_draw_polygon(config, action_id, template_arg, args):
|
||||
points = [
|
||||
@@ -395,13 +399,15 @@ ARC_PROPS = {
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.draw_arc",
|
||||
ObjUpdateAction,
|
||||
DRAW_OPA_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
**DRAW_OPA_SCHEMA,
|
||||
cv.Required(CONF_RADIUS): pixels,
|
||||
cv.Required(CONF_START_ANGLE): lv_angle_degrees,
|
||||
cv.Required(CONF_END_ANGLE): lv_angle_degrees,
|
||||
**{cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()},
|
||||
}
|
||||
).extend({cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()}),
|
||||
),
|
||||
)
|
||||
async def canvas_draw_arc(config, action_id, template_arg, args):
|
||||
radius = await size.process(config[CONF_RADIUS])
|
||||
|
||||
@@ -17,11 +17,10 @@ class CheckboxType(WidgetType):
|
||||
CONF_CHECKBOX,
|
||||
LvBoolean("lv_checkbox_t"),
|
||||
(CONF_MAIN, CONF_INDICATOR),
|
||||
TEXT_SCHEMA.extend(
|
||||
{
|
||||
**TEXT_SCHEMA,
|
||||
Optional(CONF_PAD_COLUMN): padding,
|
||||
}
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
async def to_code(self, w: Widget, config):
|
||||
|
||||
39
esphome/components/lvgl/widgets/container.py
Normal file
39
esphome/components/lvgl/widgets/container.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_HEIGHT, CONF_WIDTH
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
from ..defines import CONF_CONTAINER, CONF_MAIN, CONF_OBJ, CONF_SCROLLBAR
|
||||
from ..lv_validation import size
|
||||
from ..lvcode import lv
|
||||
from ..types import WidgetType, lv_obj_t
|
||||
|
||||
CONTAINER_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_HEIGHT, default="100%"): size,
|
||||
cv.Optional(CONF_WIDTH, default="100%"): size,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ContainerType(WidgetType):
|
||||
"""
|
||||
A simple container widget that can hold other widgets and which defaults to a 100% size.
|
||||
Made from an obj with all styles removed
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
CONF_CONTAINER,
|
||||
lv_obj_t,
|
||||
(CONF_MAIN, CONF_SCROLLBAR),
|
||||
schema=CONTAINER_SCHEMA,
|
||||
modify_schema={},
|
||||
lv_name=CONF_OBJ,
|
||||
)
|
||||
self.styles = {}
|
||||
|
||||
def on_create(self, var: MockObj, config: dict):
|
||||
lv.obj_remove_style_all(var)
|
||||
|
||||
|
||||
container_spec = ContainerType()
|
||||
@@ -23,12 +23,11 @@ class LabelType(WidgetType):
|
||||
CONF_LABEL,
|
||||
LvText("lv_label_t"),
|
||||
(CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED),
|
||||
TEXT_SCHEMA.extend(
|
||||
{
|
||||
**TEXT_SCHEMA,
|
||||
cv.Optional(CONF_RECOLOR): lv_bool,
|
||||
cv.Optional(CONF_LONG_MODE): LV_LONG_MODES.one_of,
|
||||
}
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
async def to_code(self, w: Widget, config):
|
||||
|
||||
@@ -14,13 +14,12 @@ CONF_QRCODE = "qrcode"
|
||||
CONF_DARK_COLOR = "dark_color"
|
||||
CONF_LIGHT_COLOR = "light_color"
|
||||
|
||||
QRCODE_SCHEMA = TEXT_SCHEMA.extend(
|
||||
{
|
||||
QRCODE_SCHEMA = {
|
||||
**TEXT_SCHEMA,
|
||||
cv.Optional(CONF_DARK_COLOR, default="black"): lv_color,
|
||||
cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color,
|
||||
cv.Required(CONF_SIZE): cv.int_,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class QrCodeType(WidgetType):
|
||||
|
||||
@@ -21,15 +21,14 @@ CONF_TEXTAREA = "textarea"
|
||||
|
||||
lv_textarea_t = LvText("lv_textarea_t")
|
||||
|
||||
TEXTAREA_SCHEMA = TEXT_SCHEMA.extend(
|
||||
{
|
||||
TEXTAREA_SCHEMA = {
|
||||
**TEXT_SCHEMA,
|
||||
cv.Optional(CONF_PLACEHOLDER_TEXT): lv_text,
|
||||
cv.Optional(CONF_ACCEPTED_CHARS): lv_text,
|
||||
cv.Optional(CONF_ONE_LINE): lv_bool,
|
||||
cv.Optional(CONF_PASSWORD_MODE): lv_bool,
|
||||
cv.Optional(CONF_MAX_LENGTH): lv_int,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class TextareaType(WidgetType):
|
||||
|
||||
@@ -113,7 +113,8 @@ lvgl:
|
||||
title: Messagebox
|
||||
bg_color: 0xffff
|
||||
widgets:
|
||||
- label:
|
||||
# Test single widget without list
|
||||
label:
|
||||
text: Hello Msgbox
|
||||
id: msgbox_label
|
||||
body:
|
||||
@@ -281,7 +282,7 @@ lvgl:
|
||||
#endif
|
||||
return std::string(buf);
|
||||
align: top_left
|
||||
- obj:
|
||||
- container:
|
||||
align: center
|
||||
arc_opa: COVER
|
||||
arc_color: 0xFF0000
|
||||
@@ -414,6 +415,7 @@ lvgl:
|
||||
- buttons:
|
||||
- id: button_e
|
||||
- button:
|
||||
layout: 2x1
|
||||
id: button_button
|
||||
width: 20%
|
||||
height: 10%
|
||||
@@ -430,8 +432,13 @@ lvgl:
|
||||
checked:
|
||||
bg_color: 0x000000
|
||||
widgets:
|
||||
- label:
|
||||
# Test parse a dict instead of list
|
||||
label:
|
||||
text: Button
|
||||
align: bottom_right
|
||||
image:
|
||||
src: cat_image
|
||||
align: top_left
|
||||
on_click:
|
||||
- lvgl.widget.focus: spin_up
|
||||
- lvgl.widget.focus: next
|
||||
@@ -539,6 +546,7 @@ lvgl:
|
||||
- logger.log: "tile 1 is now showing"
|
||||
tiles:
|
||||
- id: tile_1
|
||||
layout: vertical
|
||||
row: 0
|
||||
column: 0
|
||||
dir: ALL
|
||||
@@ -554,6 +562,7 @@ lvgl:
|
||||
bg_color: 0x000000
|
||||
|
||||
- id: page2
|
||||
layout: vertical
|
||||
widgets:
|
||||
- canvas:
|
||||
id: canvas_id
|
||||
@@ -1005,6 +1014,7 @@ lvgl:
|
||||
r_mod: -20
|
||||
opa: 0%
|
||||
- id: page3
|
||||
layout: horizontal
|
||||
widgets:
|
||||
- keyboard:
|
||||
id: lv_keyboard
|
||||
|
||||
Reference in New Issue
Block a user