1
0
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:
Clyde Stubbs
2025-11-04 13:39:27 +10:00
committed by GitHub
parent 758ac58343
commit 0b04361fc0
15 changed files with 572 additions and 289 deletions

View File

@@ -41,10 +41,7 @@ from .lv_validation import lv_bool, lv_images_used
from .lvcode import LvContext, LvglComponent, lvgl_static from .lvcode import LvContext, LvglComponent, lvgl_static
from .schemas import ( from .schemas import (
DISP_BG_SCHEMA, DISP_BG_SCHEMA,
FLEX_OBJ_SCHEMA,
FULL_STYLE_SCHEMA, FULL_STYLE_SCHEMA,
GRID_CELL_SCHEMA,
LAYOUT_SCHEMAS,
WIDGET_TYPES, WIDGET_TYPES,
any_widget_schema, any_widget_schema,
container_schema, container_schema,
@@ -78,6 +75,7 @@ from .widgets.button import button_spec
from .widgets.buttonmatrix import buttonmatrix_spec from .widgets.buttonmatrix import buttonmatrix_spec
from .widgets.canvas import canvas_spec from .widgets.canvas import canvas_spec
from .widgets.checkbox import checkbox_spec from .widgets.checkbox import checkbox_spec
from .widgets.container import container_spec
from .widgets.dropdown import dropdown_spec from .widgets.dropdown import dropdown_spec
from .widgets.img import img_spec from .widgets.img import img_spec
from .widgets.keyboard import keyboard_spec from .widgets.keyboard import keyboard_spec
@@ -130,20 +128,10 @@ for w_type in (
tileview_spec, tileview_spec,
qr_code_spec, qr_code_spec,
canvas_spec, canvas_spec,
container_spec,
): ):
WIDGET_TYPES[w_type.name] = w_type 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(): for w_type in WIDGET_TYPES.values():
register_action( register_action(
f"lvgl.{w_type.name}.update", f"lvgl.{w_type.name}.update",
@@ -410,7 +398,7 @@ def display_schema(config):
def add_hello_world(config): def add_hello_world(config):
if df.CONF_WIDGETS not in config and CONF_PAGES not in 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") 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 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( cv.Optional(x): validate_automation(
{ {
@@ -459,12 +448,6 @@ LVGL_SCHEMA = cv.All(
) )
for x in SIMPLE_TRIGGERS 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_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA),
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool, cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec), cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),

View File

@@ -394,6 +394,8 @@ LV_FLEX_ALIGNMENTS = LvConstant(
"SPACE_BETWEEN", "SPACE_BETWEEN",
) )
LV_FLEX_CROSS_ALIGNMENTS = LV_FLEX_ALIGNMENTS.extend("STRETCH")
LV_MENU_MODES = LvConstant( LV_MENU_MODES = LvConstant(
"LV_MENU_HEADER_", "LV_MENU_HEADER_",
"TOP_FIXED", "TOP_FIXED",
@@ -436,6 +438,7 @@ CONF_BUTTONS = "buttons"
CONF_BYTE_ORDER = "byte_order" CONF_BYTE_ORDER = "byte_order"
CONF_CHANGE_RATE = "change_rate" CONF_CHANGE_RATE = "change_rate"
CONF_CLOSE_BUTTON = "close_button" CONF_CLOSE_BUTTON = "close_button"
CONF_CONTAINER = "container"
CONF_CONTROL = "control" CONF_CONTROL = "control"
CONF_DEFAULT_FONT = "default_font" CONF_DEFAULT_FONT = "default_font"
CONF_DEFAULT_GROUP = "default_group" CONF_DEFAULT_GROUP = "default_group"

View 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(),
}
)

View File

@@ -1,3 +1,4 @@
import re
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
import esphome.codegen as cg import esphome.codegen as cg
@@ -246,6 +247,8 @@ def pixels_or_percent_validator(value):
return ["pixels", "..%"] return ["pixels", "..%"]
if isinstance(value, str) and value.lower().endswith("px"): if isinstance(value, str) and value.lower().endswith("px"):
value = cv.int_(value[:-2]) 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) value = cv.Any(cv.int_, cv.percentage)(value)
if isinstance(value, int): if isinstance(value, int):
return value return value

View File

@@ -299,6 +299,7 @@ class LvExpr(MockLv):
# Top level mock for generic lv_ calls to be recorded # Top level mock for generic lv_ calls to be recorded
lv = MockLv("lv_") lv = MockLv("lv_")
LV = MockLv("LV_")
# Just generate an expression # Just generate an expression
lv_expr = LvExpr("lv_") lv_expr = LvExpr("lv_")
# Mock for lv_obj_ calls # Mock for lv_obj_ calls
@@ -327,7 +328,7 @@ def lv_assign(target, expression):
lv_add(AssignmentExpression("", "", 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 Create but do not initialise a pointer variable
:param type: Type of the variable target :param type: Type of the variable target
@@ -343,7 +344,7 @@ def lv_Pvariable(type, name):
return var return var
def lv_variable(type, name): def lv_variable(type, name) -> MockObj:
""" """
Create but do not initialise a variable Create but do not initialise a variable
:param type: Type of the variable target :param type: Type of the variable target

View File

@@ -12,17 +12,21 @@ from esphome.const import (
CONF_TEXT, CONF_TEXT,
CONF_TIME, CONF_TIME,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_TYPE,
CONF_X, CONF_X,
CONF_Y, CONF_Y,
) )
from esphome.core import TimePeriod from esphome.core import TimePeriod
from esphome.core.config import StartupTrigger from esphome.core.config import StartupTrigger
from esphome.schema_extractors import SCHEMA_EXTRACT
from . import defines as df, lv_validation as lvalid from . import defines as df, lv_validation as lvalid
from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR, TYPE_GRID from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR
from .helpers import add_lv_use, requires_component, validate_printf 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 .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity
from .lvcode import LvglComponent, lv_event_t_ptr from .lvcode import LvglComponent, lv_event_t_ptr
from .types import ( from .types import (
@@ -72,11 +76,9 @@ def _validate_text(value):
# A schema for text properties # A schema for text properties
TEXT_SCHEMA = cv.Schema( TEXT_SCHEMA = {
{
cv.Optional(CONF_TEXT): _validate_text, cv.Optional(CONF_TEXT): _validate_text,
} }
)
LIST_ACTION_SCHEMA = cv.ensure_list( LIST_ACTION_SCHEMA = cv.ensure_list(
cv.maybe_simple_value( cv.maybe_simple_value(
@@ -136,7 +138,7 @@ STYLE_PROPS = {
"arc_opa": lvalid.opacity, "arc_opa": lvalid.opacity,
"arc_color": lvalid.lv_color, "arc_color": lvalid.lv_color,
"arc_rounded": lvalid.lv_bool, "arc_rounded": lvalid.lv_bool,
"arc_width": lvalid.lv_positive_int, "arc_width": lvalid.pixels,
"anim_time": lvalid.lv_milliseconds, "anim_time": lvalid.lv_milliseconds,
"bg_color": lvalid.lv_color, "bg_color": lvalid.lv_color,
"bg_grad": lv_gradient, "bg_grad": lv_gradient,
@@ -223,10 +225,6 @@ STYLE_REMAP = {
"image_recolor_opa": "img_recolor_opa", "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 # Complete object style schema
STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( 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 :param parts: The parts to include
:return: The schema :return: The schema
""" """
return ( return STATE_SCHEMA.extend(FLAG_SCHEMA).extend(
cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}) {cv.Optional(part): STATE_SCHEMA for part in parts}
.extend(STATE_SCHEMA)
.extend(FLAG_SCHEMA)
) )
@@ -277,10 +273,10 @@ def automation_schema(typ: LvType):
events = df.LV_EVENT_TRIGGERS + df.SWIPE_TRIGGERS events = df.LV_EVENT_TRIGGERS + df.SWIPE_TRIGGERS
if typ.has_on_value: if typ.has_on_value:
events = events + (CONF_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) args.append(lv_event_t_ptr)
return cv.Schema( return {
{ **{
cv.Optional(event): validate_automation( cv.Optional(event): validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
@@ -289,14 +285,11 @@ def automation_schema(typ: LvType):
} }
) )
for event in events for event in events
} },
).extend(
{
cv.Optional(CONF_ON_BOOT): validate_automation( cv.Optional(CONF_ON_BOOT): validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger)} {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StartupTrigger)}
) ),
} }
)
def base_update_schema(widget_type, parts): def base_update_schema(widget_type, parts):
@@ -335,75 +328,17 @@ def obj_schema(widget_type: WidgetType):
""" """
return ( return (
part_schema(widget_type.parts) part_schema(widget_type.parts)
.extend(LAYOUT_SCHEMA)
.extend(ALIGN_TO_SCHEMA) .extend(ALIGN_TO_SCHEMA)
.extend(automation_schema(widget_type.w_type)) .extend(automation_schema(widget_type.w_type))
.extend( .extend(
cv.Schema(
{ {
cv.Optional(CONF_STATE): SET_STATE_SCHEMA, cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
cv.Optional(CONF_GROUP): cv.use_id(lv_group_t), 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 = { ALIGN_TO_SCHEMA = {
cv.Optional(df.CONF_ALIGN_TO): cv.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( DISP_BG_SCHEMA = cv.Schema(
{ {
cv.Optional(df.CONF_DISP_BG_IMAGE): cv.Any( 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): 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 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. 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 :param extras: Additional options to be made available, e.g. layout properties for children
:return: The schema for this type of widget. :return: The schema for this type of widget.
""" """
@@ -549,31 +396,49 @@ def container_schema(widget_type: WidgetType, extras=None):
if extras: if extras:
schema = schema.extend(extras) schema = schema.extend(extras)
# Delayed evaluation for recursion # Delayed evaluation for recursion
return container_validator(schema, widget_type)
schema = schema.extend(widget_type.schema)
def widget_schema(widget_type: WidgetType, extras=None): def validator(value):
""" return append_layout_schema(schema, value)(value)
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
return validator
# All widget schemas must be defined before this is called.
def any_widget_schema(extras=None): 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. widget under the widgets: key.
:param extras: Additional schema to be applied to each generated one :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

View File

@@ -1,6 +1,7 @@
import sys import sys
from esphome import automation, codegen as cg 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.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE
from esphome.cpp_generator import MockObj, MockObjClass from esphome.cpp_generator import MockObj, MockObjClass
from esphome.cpp_types import esphome_ns from esphome.cpp_types import esphome_ns
@@ -135,13 +136,13 @@ class WidgetType:
self.lv_name = lv_name or name self.lv_name = lv_name or name
self.w_type = w_type self.w_type = w_type
self.parts = parts self.parts = parts
if schema is None: if not isinstance(schema, Schema):
self.schema = {} schema = Schema(schema or {})
else:
self.schema = schema self.schema = schema
if modify_schema is None: if modify_schema is None:
self.modify_schema = self.schema modify_schema = schema
else: if not isinstance(modify_schema, Schema):
modify_schema = Schema(modify_schema)
self.modify_schema = modify_schema self.modify_schema = modify_schema
self.mock_obj = MockObj(f"lv_{self.lv_name}", "_") self.mock_obj = MockObj(f"lv_{self.lv_name}", "_")
@@ -163,7 +164,6 @@ class WidgetType:
:param config: Its configuration :param config: Its configuration
:return: Generated code as a list of text lines :return: Generated code as a list of text lines
""" """
return []
async def obj_creator(self, parent: MockObjClass, config: dict): 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) 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): def get_uses(self):
""" """
Get a list of other widgets used by this one Get a list of other widgets used by this one
@@ -193,6 +200,14 @@ class WidgetType:
def get_scale(self, config: dict): def get_scale(self, config: dict):
return 1.0 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): class NumberType(WidgetType):
def get_max(self, config: dict): def get_max(self, config: dict):

View File

@@ -339,7 +339,10 @@ async def set_obj_properties(w: Widget, config):
if layout_type == TYPE_FLEX: if layout_type == TYPE_FLEX:
lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW])) lv_obj.set_flex_flow(w.obj, literal(layout[CONF_FLEX_FLOW]))
main = literal(layout[CONF_FLEX_ALIGN_MAIN]) 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]) track = literal(layout[CONF_FLEX_ALIGN_TRACK])
lv_obj.set_flex_align(w.obj, main, cross, track) lv_obj.set_flex_align(w.obj, main, cross, track)
parts = collect_parts(config) parts = collect_parts(config)
@@ -446,9 +449,11 @@ async def widget_to_code(w_cnfig, w_type: WidgetType, parent):
if spec.is_compound(): if spec.is_compound():
var = cg.new_Pvariable(wid) var = cg.new_Pvariable(wid)
lv_add(var.set_obj(creator)) lv_add(var.set_obj(creator))
spec.on_create(var.obj, w_cnfig)
else: else:
var = lv_Pvariable(lv_obj_t, wid) var = lv_Pvariable(lv_obj_t, wid)
lv_assign(var, creator) lv_assign(var, creator)
spec.on_create(var, w_cnfig)
w = Widget.create(wid, var, spec, w_cnfig) w = Widget.create(wid, var, spec, w_cnfig)
if theme := theme_widget_map.get(w_type): if theme := theme_widget_map.get(w_type):

View File

@@ -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.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
cv.Required(CONF_X): pixels, cv.Required(CONF_X): pixels,
cv.Required(CONF_Y): pixels, cv.Required(CONF_Y): pixels,
} }
) DRAW_OPA_SCHEMA = {
DRAW_OPA_SCHEMA = DRAW_SCHEMA.extend( **DRAW_SCHEMA,
{
cv.Optional(CONF_OPA): opacity, cv.Optional(CONF_OPA): opacity,
} }
)
async def draw_to_code(config, dsc_type, props, do_draw, action_id, template_arg, args): 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( @automation.register_action(
"lvgl.canvas.draw_rectangle", "lvgl.canvas.draw_rectangle",
ObjUpdateAction, ObjUpdateAction,
DRAW_SCHEMA.extend( cv.Schema(
{ {
**DRAW_OPA_SCHEMA,
cv.Required(CONF_WIDTH): cv.templatable(cv.int_), cv.Required(CONF_WIDTH): cv.templatable(cv.int_),
cv.Required(CONF_HEIGHT): cv.templatable(cv.int_), cv.Required(CONF_HEIGHT): cv.templatable(cv.int_),
}, **{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_rect(config, action_id, template_arg, args): async def canvas_draw_rect(config, action_id, template_arg, args):
width = await pixels.process(config[CONF_WIDTH]) width = await pixels.process(config[CONF_WIDTH])
@@ -261,13 +260,14 @@ TEXT_PROPS = {
@automation.register_action( @automation.register_action(
"lvgl.canvas.draw_text", "lvgl.canvas.draw_text",
ObjUpdateAction, ObjUpdateAction,
TEXT_SCHEMA.extend(DRAW_OPA_SCHEMA) cv.Schema(
.extend(
{ {
**TEXT_SCHEMA,
**DRAW_OPA_SCHEMA,
cv.Required(CONF_MAX_WIDTH): cv.templatable(cv.int_), 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): async def canvas_draw_text(config, action_id, template_arg, args):
text = await lv_text.process(config[CONF_TEXT]) text = await lv_text.process(config[CONF_TEXT])
@@ -293,13 +293,15 @@ IMG_PROPS = {
@automation.register_action( @automation.register_action(
"lvgl.canvas.draw_image", "lvgl.canvas.draw_image",
ObjUpdateAction, ObjUpdateAction,
DRAW_OPA_SCHEMA.extend( cv.Schema(
{ {
**DRAW_OPA_SCHEMA,
cv.Required(CONF_SRC): lv_image, cv.Required(CONF_SRC): lv_image,
cv.Optional(CONF_PIVOT_X, default=0): pixels, cv.Optional(CONF_PIVOT_X, default=0): pixels,
cv.Optional(CONF_PIVOT_Y, default=0): pixels, cv.Optional(CONF_PIVOT_Y, default=0): pixels,
}, **{cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()},
).extend({cv.Optional(prop): validator for prop, validator in IMG_PROPS.items()}), }
),
) )
async def canvas_draw_image(config, action_id, template_arg, args): async def canvas_draw_image(config, action_id, template_arg, args):
src = await lv_image.process(config[CONF_SRC]) 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.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
cv.Optional(CONF_OPA): opacity, cv.Optional(CONF_OPA): opacity,
cv.Required(CONF_POINTS): cv.ensure_list(point_schema), cv.Required(CONF_POINTS): cv.ensure_list(point_schema),
}, **{cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()},
).extend({cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()}), }
),
) )
async def canvas_draw_line(config, action_id, template_arg, args): async def canvas_draw_line(config, action_id, template_arg, args):
points = [ 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.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
cv.Required(CONF_POINTS): cv.ensure_list(point_schema), 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): async def canvas_draw_polygon(config, action_id, template_arg, args):
points = [ points = [
@@ -395,13 +399,15 @@ ARC_PROPS = {
@automation.register_action( @automation.register_action(
"lvgl.canvas.draw_arc", "lvgl.canvas.draw_arc",
ObjUpdateAction, ObjUpdateAction,
DRAW_OPA_SCHEMA.extend( cv.Schema(
{ {
**DRAW_OPA_SCHEMA,
cv.Required(CONF_RADIUS): pixels, cv.Required(CONF_RADIUS): pixels,
cv.Required(CONF_START_ANGLE): lv_angle_degrees, cv.Required(CONF_START_ANGLE): lv_angle_degrees,
cv.Required(CONF_END_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): async def canvas_draw_arc(config, action_id, template_arg, args):
radius = await size.process(config[CONF_RADIUS]) radius = await size.process(config[CONF_RADIUS])

View File

@@ -17,11 +17,10 @@ class CheckboxType(WidgetType):
CONF_CHECKBOX, CONF_CHECKBOX,
LvBoolean("lv_checkbox_t"), LvBoolean("lv_checkbox_t"),
(CONF_MAIN, CONF_INDICATOR), (CONF_MAIN, CONF_INDICATOR),
TEXT_SCHEMA.extend(
{ {
**TEXT_SCHEMA,
Optional(CONF_PAD_COLUMN): padding, Optional(CONF_PAD_COLUMN): padding,
} },
),
) )
async def to_code(self, w: Widget, config): async def to_code(self, w: Widget, config):

View 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()

View File

@@ -23,12 +23,11 @@ class LabelType(WidgetType):
CONF_LABEL, CONF_LABEL,
LvText("lv_label_t"), LvText("lv_label_t"),
(CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED), (CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED),
TEXT_SCHEMA.extend(
{ {
**TEXT_SCHEMA,
cv.Optional(CONF_RECOLOR): lv_bool, cv.Optional(CONF_RECOLOR): lv_bool,
cv.Optional(CONF_LONG_MODE): LV_LONG_MODES.one_of, cv.Optional(CONF_LONG_MODE): LV_LONG_MODES.one_of,
} },
),
) )
async def to_code(self, w: Widget, config): async def to_code(self, w: Widget, config):

View File

@@ -14,13 +14,12 @@ CONF_QRCODE = "qrcode"
CONF_DARK_COLOR = "dark_color" CONF_DARK_COLOR = "dark_color"
CONF_LIGHT_COLOR = "light_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_DARK_COLOR, default="black"): lv_color,
cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color, cv.Optional(CONF_LIGHT_COLOR, default="white"): lv_color,
cv.Required(CONF_SIZE): cv.int_, cv.Required(CONF_SIZE): cv.int_,
} }
)
class QrCodeType(WidgetType): class QrCodeType(WidgetType):

View File

@@ -21,15 +21,14 @@ CONF_TEXTAREA = "textarea"
lv_textarea_t = LvText("lv_textarea_t") 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_PLACEHOLDER_TEXT): lv_text,
cv.Optional(CONF_ACCEPTED_CHARS): lv_text, cv.Optional(CONF_ACCEPTED_CHARS): lv_text,
cv.Optional(CONF_ONE_LINE): lv_bool, cv.Optional(CONF_ONE_LINE): lv_bool,
cv.Optional(CONF_PASSWORD_MODE): lv_bool, cv.Optional(CONF_PASSWORD_MODE): lv_bool,
cv.Optional(CONF_MAX_LENGTH): lv_int, cv.Optional(CONF_MAX_LENGTH): lv_int,
} }
)
class TextareaType(WidgetType): class TextareaType(WidgetType):

View File

@@ -113,7 +113,8 @@ lvgl:
title: Messagebox title: Messagebox
bg_color: 0xffff bg_color: 0xffff
widgets: widgets:
- label: # Test single widget without list
label:
text: Hello Msgbox text: Hello Msgbox
id: msgbox_label id: msgbox_label
body: body:
@@ -281,7 +282,7 @@ lvgl:
#endif #endif
return std::string(buf); return std::string(buf);
align: top_left align: top_left
- obj: - container:
align: center align: center
arc_opa: COVER arc_opa: COVER
arc_color: 0xFF0000 arc_color: 0xFF0000
@@ -414,6 +415,7 @@ lvgl:
- buttons: - buttons:
- id: button_e - id: button_e
- button: - button:
layout: 2x1
id: button_button id: button_button
width: 20% width: 20%
height: 10% height: 10%
@@ -430,8 +432,13 @@ lvgl:
checked: checked:
bg_color: 0x000000 bg_color: 0x000000
widgets: widgets:
- label: # Test parse a dict instead of list
label:
text: Button text: Button
align: bottom_right
image:
src: cat_image
align: top_left
on_click: on_click:
- lvgl.widget.focus: spin_up - lvgl.widget.focus: spin_up
- lvgl.widget.focus: next - lvgl.widget.focus: next
@@ -539,6 +546,7 @@ lvgl:
- logger.log: "tile 1 is now showing" - logger.log: "tile 1 is now showing"
tiles: tiles:
- id: tile_1 - id: tile_1
layout: vertical
row: 0 row: 0
column: 0 column: 0
dir: ALL dir: ALL
@@ -554,6 +562,7 @@ lvgl:
bg_color: 0x000000 bg_color: 0x000000
- id: page2 - id: page2
layout: vertical
widgets: widgets:
- canvas: - canvas:
id: canvas_id id: canvas_id
@@ -1005,6 +1014,7 @@ lvgl:
r_mod: -20 r_mod: -20
opa: 0% opa: 0%
- id: page3 - id: page3
layout: horizontal
widgets: widgets:
- keyboard: - keyboard:
id: lv_keyboard id: lv_keyboard