mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	[lvgl] Implement canvas widget (#8504)
This commit is contained in:
		| @@ -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): | ||||
|     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 | ||||
|     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, | ||||
| ) | ||||
|     # 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,17 +14,19 @@ 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 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) | ||||
| 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): | ||||
| @@ -33,6 +37,33 @@ async def styles_to_code(config): | ||||
|             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) | ||||
|         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): | ||||
|     if theme := config.get(CONF_THEME): | ||||
|         add_lv_use(CONF_THEME) | ||||
|   | ||||
							
								
								
									
										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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user