Files
esphome-lsm6ds/esphome/components/lvgl/widgets/canvas.py
T
2341d510d3 [lvgl] Migrate to library v9.5.0 (#12312)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-03-19 20:31:33 +13:00

539 lines
16 KiB
Python

"""
LVGL 9.4 Canvas Widget Implementation
This module implements the canvas widget for LVGL 9.4. Key changes from LVGL 8.4:
1. Buffer allocation:
- LV_IMG_CF_TRUE_COLOR → LV_COLOR_FORMAT_RGB565
- LV_IMG_CF_TRUE_COLOR_ALPHA → LV_COLOR_FORMAT_ARGB8888
- LV_CANVAS_BUF_SIZE_TRUE_COLOR → LV_CANVAS_BUF_SIZE(w, h, bpp, stride)
2. Drawing API:
- All lv_canvas_draw_* functions removed
- Use layer-based drawing: lv_canvas_init_layer() / lv_canvas_finish_layer()
- Draw using low-level lv_draw_* functions (rect, line, arc, image, label)
3. Pixel operations:
- lv_canvas_set_px_color + lv_canvas_set_px_opa → lv_canvas_set_px(color, opa)
"""
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,
CONF_X,
CONF_Y,
)
from esphome.cpp_types import FixedVector
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_RADIUS,
CONF_SRC,
CONF_START_ANGLE,
addr,
get_color_formats,
literal,
)
from ..lv_validation import (
lv_angle_degrees,
lv_bool,
lv_color,
lv_image,
lv_text,
opacity,
pixels,
size,
)
from ..lvcode import LocalVariable, lv, lv_assign, lv_expr
from ..schemas import STYLE_PROPS, TEXT_SCHEMA, point_schema, remap_property
from ..types import LvType, ObjUpdateAction
from . import Widget, WidgetType, get_widgets
from .img import CONF_IMAGE
from .line import lv_point_precise_t, process_coord
CONF_CANVAS = "canvas"
CONF_BUFFER_ID = "buffer_id"
CONF_MAX_WIDTH = "max_width"
CONF_TRANSPARENT = "transparent"
CONF_DRAW_BUF_ID = "draw_buf_id"
lv_canvas_t = LvType("lv_canvas_t")
lv_draw_buf_t = LvType("lv_draw_buf_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,
cv.GenerateID(CONF_DRAW_BUF_ID): cv.declare_id(lv_draw_buf_t),
}
),
modify_schema={},
)
def get_uses(self):
return CONF_IMAGE, CONF_LABEL
async def to_code(self, w: Widget, config):
width = config[CONF_WIDTH]
height = config[CONF_HEIGHT]
# LVGL 9.4: Use LV_COLOR_FORMAT instead of LV_IMG_CF
# RGB565 is 16-bit (2 bytes per pixel), ARGB8888 is 32-bit (4 bytes per pixel)
if config[CONF_TRANSPARENT]:
color_format = "LV_COLOR_FORMAT_ARGB8888"
get_color_formats().add("ARGB8888")
else:
color_format = "LV_COLOR_FORMAT_NATIVE"
# LVGL 9.4: LV_CANVAS_BUF_SIZE(width, height, bits_per_pixel, stride)
# stride is 0 for default (width * bytes_per_pixel)
draw_buf = cg.new_Pvariable(config[CONF_DRAW_BUF_ID])
buf_size = literal(f"LV_DRAW_BUF_SIZE({width}, {height}, {color_format})")
lv.draw_buf_init(
draw_buf,
width,
height,
literal(color_format),
0,
lv_expr.malloc_core(buf_size),
literal(buf_size),
)
lv.draw_buf_set_flag(draw_buf, literal("LV_IMAGE_FLAGS_MODIFIABLE"))
lv.canvas_set_draw_buf(w.obj, draw_buf)
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,
},
),
synchronous=True,
)
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, config)
@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, default="COVER"): opacity,
cv.Required(CONF_POINTS): cv.ensure_list(point_schema),
},
),
synchronous=True,
)
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), "COVER")
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):
# LVGL 9.4: lv_canvas_set_px combines color and opacity
# Could optimize this for lambda values
for point in points:
x, y = point
lv.canvas_set_px(w.obj, x, y, color, opa)
return await action_to_code(
widget, do_set_pixels, action_id, template_arg, args, config
)
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,
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):
# LVGL 9.4: Create a layer for drawing on canvas
with LocalVariable("layer", "lv_layer_t", modifier="") as layer:
lv.canvas_init_layer(w.obj, addr(layer))
with LocalVariable("dsc", f"lv_draw_{dsc_type}_dsc_t", modifier="") as dsc:
lv.call(f"draw_{dsc_type}_dsc_init", addr(dsc))
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 = remap_property(prop)
lv_assign(getattr(dsc, mapped_prop), value)
await do_draw(addr(layer), x, y, dsc)
lv.canvas_finish_layer(w.obj, addr(layer))
return await action_to_code(
widget, action_func, action_id, template_arg, args, config
)
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_offset_x",
"shadow_offset_y",
"shadow_ofs_x",
"shadow_ofs_y",
"shadow_spread",
"shadow_opa",
)
}
def _draw_line(layer, dsc, points):
# LVGL 9.4: Use lv_draw_line for each line segment
with (
LocalVariable(
"points", FixedVector.template(lv_point_precise_t), points, modifier=""
) as points_var,
LocalVariable("i", "uint32_t", literal("0"), modifier="") as i,
):
# Draw lines between consecutive points
lv.append(
cg.RawStatement(f"for ({i} = 0; {i} != {points_var}.size() - 1; {i}++) {{")
)
lv_assign(dsc.p1, points_var[i])
lv_assign(dsc.p2, points_var[i + 1])
lv.draw_line(layer, addr(dsc))
lv.append(cg.RawStatement("}"))
@automation.register_action(
"lvgl.canvas.draw_rectangle",
ObjUpdateAction,
cv.Schema(
{
**DRAW_OPA_SCHEMA,
cv.Required(CONF_WIDTH): cv.templatable(cv.int_),
cv.Required(CONF_HEIGHT): cv.templatable(cv.int_),
**{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS},
}
),
synchronous=True,
)
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(layer, x, y, dsc):
# LVGL 9.4: Use lv_draw_rect with area
with LocalVariable("area", "lv_area_t", modifier="") as area:
lv_assign(area.x1, x)
lv_assign(area.y1, y)
lv_assign(area.x2, literal(f"{x} + {width} - 1"))
lv_assign(area.y2, literal(f"{y} + {height} - 1"))
lv.draw_rect(layer, addr(dsc), addr(area))
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,
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},
},
),
synchronous=True,
)
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(layer, x, y, dsc):
# LVGL 9.4: Use lv_draw_label with area and hint
with LocalVariable("area", "lv_area_t", modifier="") as area:
lv_assign(area.x1, x)
lv_assign(area.y1, y)
lv_assign(area.x2, literal(f"{x} + {max_width} - 1"))
lv_assign(area.y2, literal(f"{y} + LV_COORD_MAX"))
lv_assign(dsc.text, text)
lv.draw_label(layer, addr(dsc), addr(area))
return await draw_to_code(
config, "label", TEXT_PROPS, do_draw_text, action_id, template_arg, args
)
IMG_PROPS = (
"angle",
"rotation",
"scale_x",
"scale_y",
"skew_x",
"skew_y",
"scale",
"zoom",
"recolor",
"recolor_opa",
"opa",
)
def _scale_map(config):
config = {remap_property(p): v for p, v in config.items()}
if "scale" in config and {"scale_x", "scale_y"} & config.keys():
raise cv.Invalid("Cannot specify both scale and scale_x/scale_y")
if "scale" in config:
config.update({"scale_x": config["scale"], "scale_y": config["scale"]})
del config["scale"]
return config
def _get_prop_validator(prop):
return STYLE_PROPS.get(f"transform_{remap_property(prop)}") or STYLE_PROPS.get(prop)
def _prop_validator(prop):
def validator(value):
return _get_prop_validator(prop)(value)
return validator
@automation.register_action(
"lvgl.canvas.draw_image",
ObjUpdateAction,
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,
**{cv.Optional(prop): _prop_validator(prop) for prop in IMG_PROPS},
}
).add_extra(_scale_map),
synchronous=True,
)
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(layer, x, y, dsc):
# LVGL 9.4: Use lv_draw_image with area
lv_assign(dsc.src, src.get_lv_image_dsc())
if pivot_x or pivot_y:
# pylint :disable=no-member
lv_assign(dsc.pivot.x, pivot_x)
lv_assign(dsc.pivot.y, pivot_y)
with LocalVariable("area", "lv_area_t", modifier="") as area:
lv_assign(area.x1, x)
lv_assign(area.y1, y)
# Image size will be determined from the image descriptor
lv_assign(area.x2, x)
lv_assign(area.y2, y)
lv.draw_image(layer, addr(dsc), addr(area))
return await draw_to_code(
config,
"image",
{prop: _get_prop_validator(prop) for prop in 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),
**{cv.Optional(prop): validator for prop, validator in LINE_PROPS.items()},
}
),
synchronous=True,
)
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(layer, _x, _y, dsc):
_draw_line(layer, dsc, points)
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),
**{cv.Optional(prop): STYLE_PROPS[prop] for prop in RECT_PROPS},
},
),
synchronous=True,
)
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]
]
# Close the polygon
points.append(points[0])
async def do_draw_polygon(layer, x, y, dsc):
# LVGL 9.4: Draw polygon using line drawing in a closed path
# Note: This draws outline only. For filled polygons, would need different approach
# Convert rect descriptor to line descriptor for polygon outline
with LocalVariable("line_dsc", "lv_draw_line_dsc_t", modifier="") as line_dsc:
lv.draw_line_dsc_init(addr(line_dsc))
# Copy border properties from rect descriptor to line descriptor
lv_assign(line_dsc.color, dsc.border_color)
lv_assign(line_dsc.width, dsc.border_width)
lv_assign(line_dsc.opa, dsc.border_opa)
_draw_line(layer, line_dsc, points)
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,
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()},
}
),
synchronous=True,
)
async def canvas_draw_arc(config, action_id, template_arg, args):
radius = await size.process(config[CONF_RADIUS])
start_angle = await lv_angle_degrees.process(config[CONF_START_ANGLE])
end_angle = await lv_angle_degrees.process(config[CONF_END_ANGLE])
async def do_draw_arc(layer, x, y, dsc):
# LVGL 9.4: Use lv_draw_arc with center point
lv_assign(dsc.center.x, x)
lv_assign(dsc.center.y, y)
lv_assign(dsc.start_angle, start_angle)
lv_assign(dsc.end_angle, end_angle)
lv_assign(dsc.radius, radius)
lv.draw_arc(layer, addr(dsc))
return await draw_to_code(
config, "arc", ARC_PROPS, do_draw_arc, action_id, template_arg, args
)