mirror of
https://github.com/esphome/esphome.git
synced 2025-04-14 06:40:32 +01:00
[lvgl] Implement canvas widget (#8504)
This commit is contained in:
parent
8c5adfb33f
commit
1f7a84cc8e
@ -39,14 +39,13 @@ from .lvcode import LvContext, LvglComponent, lvgl_static
|
||||
from .schemas import (
|
||||
DISP_BG_SCHEMA,
|
||||
FLEX_OBJ_SCHEMA,
|
||||
FULL_STYLE_SCHEMA,
|
||||
GRID_CELL_SCHEMA,
|
||||
LAYOUT_SCHEMAS,
|
||||
STYLE_SCHEMA,
|
||||
WIDGET_TYPES,
|
||||
any_widget_schema,
|
||||
container_schema,
|
||||
create_modify_schema,
|
||||
grid_alignments,
|
||||
obj_schema,
|
||||
)
|
||||
from .styles import add_top_layer, styles_to_code, theme_to_code
|
||||
@ -74,6 +73,7 @@ from .widgets.animimg import animimg_spec
|
||||
from .widgets.arc import arc_spec
|
||||
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.dropdown import dropdown_spec
|
||||
from .widgets.img import img_spec
|
||||
@ -126,6 +126,7 @@ for w_type in (
|
||||
keyboard_spec,
|
||||
tileview_spec,
|
||||
qr_code_spec,
|
||||
canvas_spec,
|
||||
):
|
||||
WIDGET_TYPES[w_type.name] = w_type
|
||||
|
||||
@ -421,15 +422,8 @@ LVGL_SCHEMA = cv.All(
|
||||
"big_endian", "little_endian"
|
||||
),
|
||||
cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list(
|
||||
cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)})
|
||||
.extend(STYLE_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_PAD_ROW): lvalid.pixels,
|
||||
cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels,
|
||||
}
|
||||
cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)}).extend(
|
||||
FULL_STYLE_SCHEMA
|
||||
)
|
||||
),
|
||||
cv.Optional(CONF_ON_IDLE): validate_automation(
|
||||
|
@ -29,7 +29,7 @@ def add_define(macro, value="1"):
|
||||
lv_defines[macro] = value
|
||||
|
||||
|
||||
def literal(arg):
|
||||
def literal(arg) -> MockObj:
|
||||
if isinstance(arg, str):
|
||||
return MockObj(arg)
|
||||
return arg
|
||||
|
@ -254,11 +254,27 @@ def pixels_or_percent_validator(value):
|
||||
pixels_or_percent = LValidator(pixels_or_percent_validator, uint32, retmapper=literal)
|
||||
|
||||
|
||||
def zoom(value):
|
||||
def pixels_validator(value):
|
||||
if isinstance(value, str) and value.lower().endswith("px"):
|
||||
value = value[:-2]
|
||||
return cv.positive_int(value)
|
||||
|
||||
|
||||
pixels = LValidator(pixels_validator, uint32, retmapper=literal)
|
||||
|
||||
|
||||
def zoom_validator(value):
|
||||
value = cv.float_range(0.1, 10.0)(value)
|
||||
return value
|
||||
|
||||
|
||||
def zoom_retmapper(value):
|
||||
return int(value * 256)
|
||||
|
||||
|
||||
zoom = LValidator(zoom_validator, uint32, retmapper=zoom_retmapper)
|
||||
|
||||
|
||||
def angle(value):
|
||||
"""
|
||||
Validation for an angle in degrees, converted to an integer representing 0.1deg units
|
||||
@ -286,14 +302,6 @@ def size_validator(value):
|
||||
size = LValidator(size_validator, uint32, retmapper=literal)
|
||||
|
||||
|
||||
def pixels_validator(value):
|
||||
if isinstance(value, str) and value.lower().endswith("px"):
|
||||
return cv.int_(value[:-2])
|
||||
return cv.int_(value)
|
||||
|
||||
|
||||
pixels = LValidator(pixels_validator, uint32, retmapper=literal)
|
||||
|
||||
radius_consts = LvConstant("LV_RADIUS_", "CIRCLE")
|
||||
|
||||
|
||||
|
@ -206,11 +206,16 @@ class LocalVariable(MockObj):
|
||||
|
||||
def __enter__(self):
|
||||
CodeContext.start_block()
|
||||
CodeContext.append(
|
||||
VariableDeclarationExpression(self.base.type, self.modifier, self.base.id)
|
||||
)
|
||||
if self.rhs is not None:
|
||||
CodeContext.append(AssignmentExpression(None, "", self.base, self.rhs))
|
||||
CodeContext.append(
|
||||
AssignmentExpression(self.base.type, self.modifier, self.base, self.rhs)
|
||||
)
|
||||
else:
|
||||
CodeContext.append(
|
||||
VariableDeclarationExpression(
|
||||
self.base.type, self.modifier, self.base.id
|
||||
)
|
||||
)
|
||||
return MockObj(self.base)
|
||||
|
||||
def __exit__(self, *args):
|
||||
|
@ -63,6 +63,11 @@ inline void lv_disp_set_bg_image(lv_disp_t *disp, esphome::image::Image *image)
|
||||
inline void lv_obj_set_style_bg_img_src(lv_obj_t *obj, esphome::image::Image *image, lv_style_selector_t selector) {
|
||||
lv_obj_set_style_bg_img_src(obj, image->get_lv_img_dsc(), selector);
|
||||
}
|
||||
inline void lv_canvas_draw_img(lv_obj_t *canvas, lv_coord_t x, lv_coord_t y, image::Image *image,
|
||||
lv_draw_img_dsc_t *dsc) {
|
||||
lv_canvas_draw_img(canvas, x, y, image->get_lv_img_dsc(), dsc);
|
||||
}
|
||||
|
||||
#ifdef USE_LVGL_METER
|
||||
inline lv_meter_indicator_t *lv_meter_add_needle_img(lv_obj_t *obj, lv_meter_scale_t *scale, esphome::image::Image *src,
|
||||
lv_coord_t pivot_x, lv_coord_t pivot_y) {
|
||||
|
@ -87,31 +87,29 @@ ENCODER_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
POINT_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_X): cv.templatable(cv.int_),
|
||||
cv.Required(CONF_Y): cv.templatable(cv.int_),
|
||||
}
|
||||
)
|
||||
|
||||
def point_shorthand(value):
|
||||
|
||||
def point_schema(value):
|
||||
"""
|
||||
A shorthand for a point in the form of x,y
|
||||
:param value: The value to check
|
||||
:return: The value as a tuple of x,y
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
x, y = map(int, value.split(","))
|
||||
return {CONF_X: x, CONF_Y: y}
|
||||
except ValueError:
|
||||
pass
|
||||
raise cv.Invalid("Invalid point format, should be <x_value>, <y_value>")
|
||||
|
||||
|
||||
POINT_SCHEMA = cv.Any(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_X): cv.templatable(cv.int_),
|
||||
cv.Required(CONF_Y): cv.templatable(cv.int_),
|
||||
}
|
||||
),
|
||||
point_shorthand,
|
||||
)
|
||||
if isinstance(value, dict):
|
||||
return POINT_SCHEMA(value)
|
||||
try:
|
||||
x, y = map(int, value.split(","))
|
||||
return {CONF_X: x, CONF_Y: y}
|
||||
except ValueError:
|
||||
pass
|
||||
# not raising this in the catch block because pylint doesn't like it
|
||||
raise cv.Invalid("Invalid point format, should be <x_int>, <y_int>")
|
||||
|
||||
|
||||
# All LVGL styles and their validators
|
||||
@ -132,6 +130,7 @@ STYLE_PROPS = {
|
||||
"bg_image_recolor": lvalid.lv_color,
|
||||
"bg_image_recolor_opa": lvalid.opacity,
|
||||
"bg_image_src": lvalid.lv_image,
|
||||
"bg_image_tiled": lvalid.lv_bool,
|
||||
"bg_main_stop": lvalid.stop_value,
|
||||
"bg_opa": lvalid.opacity,
|
||||
"border_color": lvalid.lv_color,
|
||||
@ -146,9 +145,9 @@ STYLE_PROPS = {
|
||||
"height": lvalid.size,
|
||||
"image_recolor": lvalid.lv_color,
|
||||
"image_recolor_opa": lvalid.opacity,
|
||||
"line_width": cv.positive_int,
|
||||
"line_dash_width": cv.positive_int,
|
||||
"line_dash_gap": cv.positive_int,
|
||||
"line_width": lvalid.lv_positive_int,
|
||||
"line_dash_width": lvalid.lv_positive_int,
|
||||
"line_dash_gap": lvalid.lv_positive_int,
|
||||
"line_rounded": lvalid.lv_bool,
|
||||
"line_color": lvalid.lv_color,
|
||||
"opa": lvalid.opacity,
|
||||
@ -176,8 +175,8 @@ STYLE_PROPS = {
|
||||
"LV_TEXT_DECOR_", "NONE", "UNDERLINE", "STRIKETHROUGH"
|
||||
).several_of,
|
||||
"text_font": lv_font,
|
||||
"text_letter_space": cv.positive_int,
|
||||
"text_line_space": cv.positive_int,
|
||||
"text_letter_space": lvalid.lv_positive_int,
|
||||
"text_line_space": lvalid.lv_positive_int,
|
||||
"text_opa": lvalid.opacity,
|
||||
"transform_angle": lvalid.lv_angle,
|
||||
"transform_height": lvalid.pixels_or_percent,
|
||||
@ -201,10 +200,15 @@ STYLE_REMAP = {
|
||||
"bg_image_recolor": "bg_img_recolor",
|
||||
"bg_image_recolor_opa": "bg_img_recolor_opa",
|
||||
"bg_image_src": "bg_img_src",
|
||||
"bg_image_tiled": "bg_img_tiled",
|
||||
"image_recolor": "img_recolor",
|
||||
"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(
|
||||
{
|
||||
@ -215,6 +219,16 @@ STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).ex
|
||||
}
|
||||
)
|
||||
|
||||
# Also allow widget specific properties for use in style definitions
|
||||
FULL_STYLE_SCHEMA = STYLE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_PAD_ROW): lvalid.pixels,
|
||||
cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels,
|
||||
}
|
||||
)
|
||||
|
||||
# Object states. Top level properties apply to MAIN
|
||||
STATE_SCHEMA = cv.Schema(
|
||||
{cv.Optional(state): STYLE_SCHEMA for state in df.STATES}
|
||||
@ -346,10 +360,6 @@ grid_spec = cv.Any(
|
||||
lvalid.size, df.LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space
|
||||
)
|
||||
|
||||
cell_alignments = df.LV_CELL_ALIGNMENTS.one_of
|
||||
grid_alignments = df.LV_GRID_ALIGNMENTS.one_of
|
||||
flex_alignments = df.LV_FLEX_ALIGNMENTS.one_of
|
||||
|
||||
LAYOUT_SCHEMA = {
|
||||
cv.Optional(df.CONF_LAYOUT): cv.typed_schema(
|
||||
{
|
||||
|
@ -1,4 +1,6 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
@ -12,25 +14,54 @@ from .defines import (
|
||||
)
|
||||
from .helpers import add_lv_use
|
||||
from .lvcode import LambdaContext, LocalVariable, lv, lv_assign, lv_variable
|
||||
from .schemas import ALL_STYLES, STYLE_REMAP
|
||||
from .types import lv_lambda_t, lv_obj_t, lv_obj_t_ptr
|
||||
from .widgets import Widget, add_widgets, set_obj_properties, theme_widget_map
|
||||
from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, STYLE_REMAP
|
||||
from .types import ObjUpdateAction, lv_lambda_t, lv_obj_t, lv_obj_t_ptr, lv_style_t
|
||||
from .widgets import (
|
||||
Widget,
|
||||
add_widgets,
|
||||
set_obj_properties,
|
||||
theme_widget_map,
|
||||
wait_for_widgets,
|
||||
)
|
||||
from .widgets.obj import obj_spec
|
||||
|
||||
|
||||
async def style_set(svar, style):
|
||||
for prop, validator in ALL_STYLES.items():
|
||||
if (value := style.get(prop)) is not None:
|
||||
if isinstance(validator, LValidator):
|
||||
value = await validator.process(value)
|
||||
if isinstance(value, list):
|
||||
value = "|".join(value)
|
||||
remapped_prop = STYLE_REMAP.get(prop, prop)
|
||||
lv.call(f"style_set_{remapped_prop}", svar, literal(value))
|
||||
|
||||
|
||||
async def styles_to_code(config):
|
||||
"""Convert styles to C__ code."""
|
||||
for style in config.get(CONF_STYLE_DEFINITIONS, ()):
|
||||
svar = cg.new_Pvariable(style[CONF_ID])
|
||||
lv.style_init(svar)
|
||||
for prop, validator in ALL_STYLES.items():
|
||||
if (value := style.get(prop)) is not None:
|
||||
if isinstance(validator, LValidator):
|
||||
value = await validator.process(value)
|
||||
if isinstance(value, list):
|
||||
value = "|".join(value)
|
||||
remapped_prop = STYLE_REMAP.get(prop, prop)
|
||||
lv.call(f"style_set_{remapped_prop}", svar, literal(value))
|
||||
await style_set(svar, style)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.style.update",
|
||||
ObjUpdateAction,
|
||||
FULL_STYLE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(lv_style_t),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def style_update_to_code(config, action_id, template_arg, args):
|
||||
await wait_for_widgets()
|
||||
style = await cg.get_variable(config[CONF_ID])
|
||||
async with LambdaContext(parameters=args, where=action_id) as context:
|
||||
await style_set(style, config)
|
||||
|
||||
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
|
||||
return var
|
||||
|
||||
|
||||
async def theme_to_code(config):
|
||||
|
403
esphome/components/lvgl/widgets/canvas.py
Normal file
403
esphome/components/lvgl/widgets/canvas.py
Normal file
@ -0,0 +1,403 @@
|
||||
from esphome import automation, codegen as cg, config_validation as cv
|
||||
from esphome.components.display_menu_base import CONF_LABEL
|
||||
from esphome.const import CONF_COLOR, CONF_HEIGHT, CONF_ID, CONF_TEXT, CONF_WIDTH
|
||||
from esphome.cpp_generator import Literal, MockObj
|
||||
|
||||
from ..automation import action_to_code
|
||||
from ..defines import (
|
||||
CONF_END_ANGLE,
|
||||
CONF_MAIN,
|
||||
CONF_OPA,
|
||||
CONF_PIVOT_X,
|
||||
CONF_PIVOT_Y,
|
||||
CONF_POINTS,
|
||||
CONF_SRC,
|
||||
CONF_START_ANGLE,
|
||||
CONF_X,
|
||||
CONF_Y,
|
||||
literal,
|
||||
)
|
||||
from ..lv_validation import (
|
||||
lv_angle,
|
||||
lv_bool,
|
||||
lv_color,
|
||||
lv_image,
|
||||
lv_text,
|
||||
opacity,
|
||||
pixels,
|
||||
size,
|
||||
)
|
||||
from ..lvcode import LocalVariable, lv, lv_assign
|
||||
from ..schemas import STYLE_PROPS, STYLE_REMAP, TEXT_SCHEMA, point_schema
|
||||
from ..types import LvType, ObjUpdateAction, WidgetType
|
||||
from . import Widget, get_widgets
|
||||
from .line import lv_point_t, process_coord
|
||||
|
||||
CONF_CANVAS = "canvas"
|
||||
CONF_BUFFER_ID = "buffer_id"
|
||||
CONF_MAX_WIDTH = "max_width"
|
||||
CONF_TRANSPARENT = "transparent"
|
||||
CONF_RADIUS = "radius"
|
||||
|
||||
lv_canvas_t = LvType("lv_canvas_t")
|
||||
|
||||
|
||||
class CanvasType(WidgetType):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
CONF_CANVAS,
|
||||
lv_canvas_t,
|
||||
(CONF_MAIN,),
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_WIDTH): size,
|
||||
cv.Required(CONF_HEIGHT): size,
|
||||
cv.Optional(CONF_TRANSPARENT, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
def get_uses(self):
|
||||
return "img", CONF_LABEL
|
||||
|
||||
async def to_code(self, w: Widget, config):
|
||||
width = config[CONF_WIDTH]
|
||||
height = config[CONF_HEIGHT]
|
||||
use_alpha = "_ALPHA" if config[CONF_TRANSPARENT] else ""
|
||||
lv.canvas_set_buffer(
|
||||
w.obj,
|
||||
lv.custom_mem_alloc(
|
||||
literal(f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})")
|
||||
),
|
||||
width,
|
||||
height,
|
||||
literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"),
|
||||
)
|
||||
|
||||
|
||||
canvas_spec = CanvasType()
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.fill",
|
||||
ObjUpdateAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
|
||||
cv.Required(CONF_COLOR): lv_color,
|
||||
cv.Optional(CONF_OPA, default="COVER"): opacity,
|
||||
},
|
||||
),
|
||||
)
|
||||
async def canvas_fill(config, action_id, template_arg, args):
|
||||
widget = await get_widgets(config)
|
||||
color = await lv_color.process(config[CONF_COLOR])
|
||||
opa = await opacity.process(config[CONF_OPA])
|
||||
|
||||
async def do_fill(w: Widget):
|
||||
lv.canvas_fill_bg(w.obj, color, opa)
|
||||
|
||||
return await action_to_code(widget, do_fill, action_id, template_arg, args)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.set_pixels",
|
||||
ObjUpdateAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
|
||||
cv.Required(CONF_COLOR): lv_color,
|
||||
cv.Optional(CONF_OPA): opacity,
|
||||
cv.Required(CONF_POINTS): cv.ensure_list(point_schema),
|
||||
},
|
||||
),
|
||||
)
|
||||
async def canvas_set_pixel(config, action_id, template_arg, args):
|
||||
widget = await get_widgets(config)
|
||||
color = await lv_color.process(config[CONF_COLOR])
|
||||
opa = await opacity.process(config.get(CONF_OPA))
|
||||
points = [
|
||||
(
|
||||
await pixels.process(p[CONF_X]),
|
||||
await pixels.process(p[CONF_Y]),
|
||||
)
|
||||
for p in config[CONF_POINTS]
|
||||
]
|
||||
|
||||
async def do_set_pixels(w: Widget):
|
||||
if isinstance(color, MockObj):
|
||||
for point in points:
|
||||
x, y = point
|
||||
lv.canvas_set_px_color(w.obj, x, y, color)
|
||||
else:
|
||||
with LocalVariable("color", "lv_color_t", color, modifier="") as color_var:
|
||||
for point in points:
|
||||
x, y = point
|
||||
lv.canvas_set_px_color(w.obj, x, y, color_var)
|
||||
if opa:
|
||||
if isinstance(opa, Literal):
|
||||
for point in points:
|
||||
x, y = point
|
||||
lv.canvas_set_px_opa(w.obj, x, y, opa)
|
||||
else:
|
||||
with LocalVariable("opa", "lv_opa_t", opa, modifier="") as opa_var:
|
||||
for point in points:
|
||||
x, y = point
|
||||
lv.canvas_set_px_opa(w.obj, x, y, opa_var)
|
||||
|
||||
return await action_to_code(widget, do_set_pixels, action_id, template_arg, args)
|
||||
|
||||
|
||||
DRAW_SCHEMA = cv.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(
|
||||
{
|
||||
cv.Optional(CONF_OPA): opacity,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def draw_to_code(config, dsc_type, props, do_draw, action_id, template_arg, args):
|
||||
widget = await get_widgets(config)
|
||||
x = await pixels.process(config.get(CONF_X))
|
||||
y = await pixels.process(config.get(CONF_Y))
|
||||
|
||||
async def action_func(w: Widget):
|
||||
with LocalVariable("dsc", f"lv_draw_{dsc_type}_dsc_t", modifier="") as dsc:
|
||||
dsc_addr = literal(f"&{dsc}")
|
||||
lv.call(f"draw_{dsc_type}_dsc_init", dsc_addr)
|
||||
if CONF_OPA in config:
|
||||
opa = await opacity.process(config[CONF_OPA])
|
||||
lv_assign(dsc.opa, opa)
|
||||
for prop, validator in props.items():
|
||||
if prop in config:
|
||||
value = await validator.process(config[prop])
|
||||
mapped_prop = STYLE_REMAP.get(prop, prop)
|
||||
lv_assign(getattr(dsc, mapped_prop), value)
|
||||
await do_draw(w, x, y, dsc_addr)
|
||||
|
||||
return await action_to_code(widget, action_func, action_id, template_arg, args)
|
||||
|
||||
|
||||
RECT_PROPS = {
|
||||
p: STYLE_PROPS[p]
|
||||
for p in (
|
||||
"radius",
|
||||
"bg_opa",
|
||||
"bg_color",
|
||||
"bg_grad",
|
||||
"border_color",
|
||||
"border_width",
|
||||
"border_opa",
|
||||
"outline_color",
|
||||
"outline_width",
|
||||
"outline_pad",
|
||||
"outline_opa",
|
||||
"shadow_color",
|
||||
"shadow_width",
|
||||
"shadow_ofs_x",
|
||||
"shadow_ofs_y",
|
||||
"shadow_spread",
|
||||
"shadow_opa",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.draw_rectangle",
|
||||
ObjUpdateAction,
|
||||
DRAW_SCHEMA.extend(
|
||||
{
|
||||
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}),
|
||||
)
|
||||
async def canvas_draw_rect(config, action_id, template_arg, args):
|
||||
width = await pixels.process(config[CONF_WIDTH])
|
||||
height = await pixels.process(config[CONF_HEIGHT])
|
||||
|
||||
async def do_draw_rect(w: Widget, x, y, dsc_addr):
|
||||
lv.canvas_draw_rect(w.obj, x, y, width, height, dsc_addr)
|
||||
|
||||
return await draw_to_code(
|
||||
config, "rect", RECT_PROPS, do_draw_rect, action_id, template_arg, args
|
||||
)
|
||||
|
||||
|
||||
TEXT_PROPS = {
|
||||
p: STYLE_PROPS[f"text_{p}"]
|
||||
for p in (
|
||||
"font",
|
||||
"color",
|
||||
# "sel_color",
|
||||
# "sel_bg_color",
|
||||
"line_space",
|
||||
"letter_space",
|
||||
"align",
|
||||
"decor",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.draw_text",
|
||||
ObjUpdateAction,
|
||||
TEXT_SCHEMA.extend(DRAW_OPA_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_MAX_WIDTH): cv.templatable(cv.int_),
|
||||
},
|
||||
)
|
||||
.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])
|
||||
max_width = await pixels.process(config[CONF_MAX_WIDTH])
|
||||
|
||||
async def do_draw_text(w: Widget, x, y, dsc_addr):
|
||||
lv.canvas_draw_text(w.obj, x, y, max_width, dsc_addr, text)
|
||||
|
||||
return await draw_to_code(
|
||||
config, "label", TEXT_PROPS, do_draw_text, action_id, template_arg, args
|
||||
)
|
||||
|
||||
|
||||
IMG_PROPS = {
|
||||
"angle": STYLE_PROPS["transform_angle"],
|
||||
"zoom": STYLE_PROPS["transform_zoom"],
|
||||
"recolor": STYLE_PROPS["image_recolor"],
|
||||
"recolor_opa": STYLE_PROPS["image_recolor_opa"],
|
||||
"opa": STYLE_PROPS["opa"],
|
||||
}
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.draw_image",
|
||||
ObjUpdateAction,
|
||||
DRAW_OPA_SCHEMA.extend(
|
||||
{
|
||||
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()}),
|
||||
)
|
||||
async def canvas_draw_image(config, action_id, template_arg, args):
|
||||
src = await lv_image.process(config[CONF_SRC])
|
||||
pivot_x = await pixels.process(config[CONF_PIVOT_X])
|
||||
pivot_y = await pixels.process(config[CONF_PIVOT_Y])
|
||||
|
||||
async def do_draw_image(w: Widget, x, y, dsc_addr):
|
||||
dsc = MockObj(f"(*{dsc_addr})")
|
||||
if pivot_x or pivot_y:
|
||||
# pylint :disable=no-member
|
||||
lv_assign(dsc.pivot, literal(f"{{{pivot_x}, {pivot_y}}}"))
|
||||
lv.canvas_draw_img(w.obj, x, y, src, dsc_addr)
|
||||
|
||||
return await draw_to_code(
|
||||
config, "img", IMG_PROPS, do_draw_image, action_id, template_arg, args
|
||||
)
|
||||
|
||||
|
||||
LINE_PROPS = {
|
||||
"width": STYLE_PROPS["line_width"],
|
||||
"color": STYLE_PROPS["line_color"],
|
||||
"dash-width": STYLE_PROPS["line_dash_width"],
|
||||
"dash-gap": STYLE_PROPS["line_dash_gap"],
|
||||
"round_start": lv_bool,
|
||||
"round_end": lv_bool,
|
||||
}
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.draw_line",
|
||||
ObjUpdateAction,
|
||||
cv.Schema(
|
||||
{
|
||||
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()}),
|
||||
)
|
||||
async def canvas_draw_line(config, action_id, template_arg, args):
|
||||
points = [
|
||||
[await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])]
|
||||
for p in config[CONF_POINTS]
|
||||
]
|
||||
|
||||
async def do_draw_line(w: Widget, x, y, dsc_addr):
|
||||
with LocalVariable(
|
||||
"points", cg.std_vector.template(lv_point_t), points, modifier=""
|
||||
) as points_var:
|
||||
lv.canvas_draw_line(w.obj, points_var.data(), points_var.size(), dsc_addr)
|
||||
|
||||
return await draw_to_code(
|
||||
config, "line", LINE_PROPS, do_draw_line, action_id, template_arg, args
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.draw_polygon",
|
||||
ObjUpdateAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(lv_canvas_t),
|
||||
cv.Required(CONF_POINTS): cv.ensure_list(point_schema),
|
||||
},
|
||||
).extend({cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS}),
|
||||
)
|
||||
async def canvas_draw_polygon(config, action_id, template_arg, args):
|
||||
points = [
|
||||
[await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])]
|
||||
for p in config[CONF_POINTS]
|
||||
]
|
||||
|
||||
async def do_draw_polygon(w: Widget, x, y, dsc_addr):
|
||||
with LocalVariable(
|
||||
"points", cg.std_vector.template(lv_point_t), points, modifier=""
|
||||
) as points_var:
|
||||
lv.canvas_draw_polygon(
|
||||
w.obj, points_var.data(), points_var.size(), dsc_addr
|
||||
)
|
||||
|
||||
return await draw_to_code(
|
||||
config, "rect", RECT_PROPS, do_draw_polygon, action_id, template_arg, args
|
||||
)
|
||||
|
||||
|
||||
ARC_PROPS = {
|
||||
"width": STYLE_PROPS["arc_width"],
|
||||
"color": STYLE_PROPS["arc_color"],
|
||||
"rounded": STYLE_PROPS["arc_rounded"],
|
||||
}
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.canvas.draw_arc",
|
||||
ObjUpdateAction,
|
||||
DRAW_OPA_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_RADIUS): pixels,
|
||||
cv.Required(CONF_START_ANGLE): lv_angle,
|
||||
cv.Required(CONF_END_ANGLE): lv_angle,
|
||||
}
|
||||
).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])
|
||||
start_angle = await lv_angle.process(config[CONF_START_ANGLE])
|
||||
end_angle = await lv_angle.process(config[CONF_END_ANGLE])
|
||||
|
||||
async def do_draw_arc(w: Widget, x, y, dsc_addr):
|
||||
lv.canvas_draw_arc(w.obj, x, y, radius, start_angle, end_angle, dsc_addr)
|
||||
|
||||
return await draw_to_code(
|
||||
config, "arc", ARC_PROPS, do_draw_arc, action_id, template_arg, args
|
||||
)
|
@ -4,7 +4,7 @@ from esphome.core import Lambda
|
||||
|
||||
from ..defines import CONF_MAIN, CONF_X, CONF_Y, call_lambda
|
||||
from ..lvcode import lv_add
|
||||
from ..schemas import POINT_SCHEMA
|
||||
from ..schemas import point_schema
|
||||
from ..types import LvCompound, LvType
|
||||
from . import Widget, WidgetType
|
||||
|
||||
@ -16,14 +16,14 @@ lv_point_t = cg.global_ns.struct("lv_point_t")
|
||||
|
||||
|
||||
LINE_SCHEMA = {
|
||||
cv.Required(CONF_POINTS): cv.ensure_list(POINT_SCHEMA),
|
||||
cv.Required(CONF_POINTS): cv.ensure_list(point_schema),
|
||||
}
|
||||
|
||||
|
||||
async def process_coord(coord):
|
||||
if isinstance(coord, Lambda):
|
||||
coord = call_lambda(
|
||||
await cg.process_lambda(coord, (), return_type="lv_coord_t")
|
||||
await cg.process_lambda(coord, [], return_type="lv_coord_t")
|
||||
)
|
||||
if not coord.endswith("()"):
|
||||
coord = f"static_cast<lv_coord_t>({coord})"
|
||||
|
@ -130,6 +130,10 @@ lvgl:
|
||||
on_click:
|
||||
then:
|
||||
- lvgl.widget.hide: message_box
|
||||
- lvgl.style.update:
|
||||
id: style_test
|
||||
bg_color: blue
|
||||
bg_opa: !lambda return 0.5;
|
||||
- id: simple_msgbox
|
||||
title: Simple
|
||||
|
||||
@ -510,6 +514,110 @@ lvgl:
|
||||
|
||||
- id: page2
|
||||
widgets:
|
||||
- canvas:
|
||||
id: canvas_id
|
||||
align: center
|
||||
width: 400
|
||||
height: 400
|
||||
transparent: true
|
||||
on_boot:
|
||||
- lvgl.canvas.fill:
|
||||
color: blue
|
||||
opa: 50%
|
||||
- lvgl.canvas.draw_rectangle:
|
||||
x: 20
|
||||
y: 20
|
||||
width: 150
|
||||
height: 150
|
||||
bg_color: green
|
||||
bg_opa: cover
|
||||
radius: 5
|
||||
border_color: black
|
||||
border_width: 4
|
||||
border_opa: 80%
|
||||
shadow_color: black
|
||||
shadow_width: 10
|
||||
shadow_ofs_x: 5
|
||||
shadow_ofs_y: 5
|
||||
shadow_spread: 4
|
||||
shadow_opa: cover
|
||||
outline_color: red
|
||||
outline_width: 4
|
||||
outline_pad: 4
|
||||
outline_opa: cover
|
||||
- lvgl.canvas.set_pixels:
|
||||
color: red
|
||||
points:
|
||||
- x: 100
|
||||
y: 100
|
||||
- 100,101
|
||||
- 100,102
|
||||
- 100,103
|
||||
- 100,104
|
||||
- lvgl.canvas.set_pixels:
|
||||
opa: 50%
|
||||
color: !lambda return lv_color_make(255,255,255);
|
||||
points:
|
||||
- x: !lambda return random_uint32() % 200;
|
||||
y: !lambda return random_uint32() % 200;
|
||||
- 121,120
|
||||
- 122,120
|
||||
- 123,120
|
||||
- 124,120
|
||||
- 125,120
|
||||
|
||||
- lvgl.canvas.draw_text:
|
||||
x: 100
|
||||
y: 100
|
||||
font: montserrat_18
|
||||
color: white
|
||||
opa: cover
|
||||
decor: underline
|
||||
letter_space: 1
|
||||
line_space: 2
|
||||
text: Canvas Text
|
||||
align: center
|
||||
max_width: 150
|
||||
- lvgl.canvas.draw_image:
|
||||
src: cat_image
|
||||
x: 100
|
||||
y: 100
|
||||
angle: 90
|
||||
zoom: 2.0
|
||||
pivot_x: 25
|
||||
pivot_y: 25
|
||||
- lvgl.canvas.draw_line:
|
||||
color: blue
|
||||
width: 4
|
||||
round_end: true
|
||||
round_start: false
|
||||
points:
|
||||
- 50,50
|
||||
- 50, 200
|
||||
- 200, 200
|
||||
- 200, 50
|
||||
- 50,50
|
||||
- lvgl.canvas.draw_polygon:
|
||||
bg_color: teal
|
||||
border_color: white
|
||||
border_width: 2
|
||||
border_opa: cover
|
||||
points:
|
||||
- 150,150
|
||||
- 150, 300
|
||||
- 300, 300
|
||||
- 350, 250
|
||||
- lvgl.canvas.draw_arc:
|
||||
x: 200
|
||||
y: 200
|
||||
radius: 40
|
||||
opa: 50%
|
||||
color: purple
|
||||
width: 6
|
||||
rounded: true
|
||||
start_angle: 10
|
||||
end_angle: !lambda return 900;
|
||||
|
||||
- qrcode:
|
||||
id: lv_qr
|
||||
align: left_mid
|
||||
|
Loading…
x
Reference in New Issue
Block a user