From 66b6d36a260dd1900c1786f9edc98c47157c8255 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 1 Apr 2026 07:04:10 +1000 Subject: [PATCH] [lvgl] Fixes #3 (#15304) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/lvgl/__init__.py | 10 ++----- esphome/components/lvgl/defines.py | 4 --- esphome/components/lvgl/styles.py | 31 +++------------------ esphome/components/lvgl/widgets/__init__.py | 7 +++-- esphome/components/lvgl/widgets/canvas.py | 2 -- esphome/components/lvgl/widgets/keyboard.py | 26 +++++++++++++---- esphome/components/lvgl/widgets/label.py | 2 +- esphome/components/lvgl/widgets/line.py | 19 ++++++------- esphome/components/lvgl/widgets/msgbox.py | 8 ++++-- esphome/components/lvgl/widgets/qrcode.py | 3 +- esphome/components/lvgl/widgets/tabview.py | 4 +-- 11 files changed, 48 insertions(+), 68 deletions(-) diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 6377183ef4f..736fba759f3 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -380,7 +380,8 @@ async def to_code(configs): # This must be done after all widgets are created for comp in helpers.lvgl_components_required: cg.add_define(f"USE_LVGL_{comp.upper()}") - lv_image_formats = df.get_color_formats().copy() + # Currently always need RGB565 for the display buffer, and ARGB8888 is used for layer blending + lv_image_formats = {"RGB565", "ARGB8888"} if { "transform_rotation", "transform_scale", @@ -388,10 +389,6 @@ async def to_code(configs): "transform_scale_y", } & styles_used: df.add_define("LV_COLOR_SCREEN_TRANSP", "1") - lv_image_formats.add("ARGB8888") - lv_image_formats.add( - "RGB565" - ) # Currently always need RGB565 for the display buffer for use in helpers.lv_uses: df.add_define(f"LV_USE_{use.upper()}") cg.add_define(f"USE_LVGL_{use.upper()}") @@ -401,9 +398,6 @@ async def to_code(configs): metadata = get_image_metadata(image_id.id) image_type = IMAGE_TYPE[metadata.image_type] transparent = metadata.transparency != CONF_OPAQUE - if transparent: - # Internal draw layer will use ARGB8888 - lv_image_formats.add("ARGB8888") if image_type == ImageBinary: lv_image_formats.add("I1") if image_type == ImageGrayscale: diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index de5835d7a69..dd51a2f5199 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -52,10 +52,6 @@ def get_remapped_uses(): return get_data(KEY_REMAPPED_USES, set()) -def get_color_formats(): - return get_data(KEY_COLOR_FORMATS, set()) - - def add_warning(msg: str): get_warnings().add(msg) diff --git a/esphome/components/lvgl/styles.py b/esphome/components/lvgl/styles.py index 6f43e78f90e..793290de73a 100644 --- a/esphome/components/lvgl/styles.py +++ b/esphome/components/lvgl/styles.py @@ -4,26 +4,12 @@ import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.core import ID -from .defines import ( - CONF_STYLE_DEFINITIONS, - CONF_THEME, - CONF_TOP_LAYER, - LValidator, - literal, -) +from .defines import CONF_STYLE_DEFINITIONS, CONF_THEME, LValidator, literal from .helpers import add_lv_use -from .lvcode import LambdaContext, LocalVariable, lv +from .lvcode import LambdaContext, lv from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, remap_property -from .types import ObjUpdateAction, lv_obj_t, lv_style_t -from .widgets import ( - Widget, - add_widgets, - collect_parts, - set_obj_properties, - theme_widget_map, - wait_for_widgets, -) -from .widgets.obj import obj_spec +from .types import ObjUpdateAction, lv_style_t +from .widgets import collect_parts, theme_widget_map, wait_for_widgets def has_style_props(config) -> bool: @@ -112,12 +98,3 @@ async def theme_to_code(config): for state, props in states.items() } theme_widget_map[w_name] = styles - - -async def add_top_layer(lv_component, config): - top_layer = lv.disp_get_layer_top(lv_component.var.get_disp()) - if top_conf := config.get(CONF_TOP_LAYER): - with LocalVariable("top_layer", lv_obj_t, top_layer) as top_layer_obj: - top_w = Widget(top_layer_obj, obj_spec, top_conf) - await set_obj_properties(top_w, top_conf) - await add_widgets(top_w, top_conf) diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index b383196963c..0ac4062106d 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -1,5 +1,4 @@ import sys -from typing import Any from esphome import codegen as cg, config_validation as cv from esphome.automation import register_action @@ -405,7 +404,11 @@ class Widget: # Map of widgets to their config, used for trigger generation -widget_map: dict[Any, Widget] = {} +widget_map: dict[ID, Widget] = {} + + +def is_widget_completed(name: ID) -> bool: + return name in widget_map class LvScrActType(WidgetType): diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index 0e40d0dfbef..f12766bae12 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -42,7 +42,6 @@ from ..defines import ( CONF_SRC, CONF_START_ANGLE, addr, - get_color_formats, literal, ) from ..lv_validation import ( @@ -99,7 +98,6 @@ class CanvasType(WidgetType): # 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" diff --git a/esphome/components/lvgl/widgets/keyboard.py b/esphome/components/lvgl/widgets/keyboard.py index d4a71078d0b..029ca5f6846 100644 --- a/esphome/components/lvgl/widgets/keyboard.py +++ b/esphome/components/lvgl/widgets/keyboard.py @@ -1,12 +1,15 @@ from esphome.components.key_provider import KeyProvider import esphome.config_validation as cv from esphome.const import CONF_ITEMS, CONF_MODE +from esphome.core import CORE from esphome.cpp_types import std_string +from .. import LvContext from ..defines import CONF_MAIN, KEYBOARD_MODES, literal -from ..helpers import add_lv_use, lvgl_components_required +from ..helpers import lvgl_components_required from ..types import LvCompound, LvType -from . import Widget, WidgetType, get_widgets +from . import Widget, WidgetType, get_widgets, is_widget_completed +from .buttonmatrix import CONF_BUTTONMATRIX from .textarea import CONF_TEXTAREA, lv_textarea_t CONF_KEYBOARD = "keyboard" @@ -41,16 +44,27 @@ class KeyboardType(WidgetType): ) def get_uses(self): - return CONF_KEYBOARD, CONF_TEXTAREA + return CONF_KEYBOARD, CONF_TEXTAREA, CONF_BUTTONMATRIX async def to_code(self, w: Widget, config: dict): lvgl_components_required.add("KEY_LISTENER") lvgl_components_required.add(CONF_KEYBOARD) - add_lv_use("btnmatrix") if mode := config.get(CONF_MODE): await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode)) - if ta := await get_widgets(config, CONF_TEXTAREA): - await w.set_property(CONF_TEXTAREA, ta[0].obj) + if textarea := config.get(CONF_TEXTAREA): + # If a textarea is configured, it must be generated before the keyboard can attach it. + # If not yet configured, defer the attachment code. + + async def add_textarea(): + async with LvContext(): + await w.set_property( + CONF_TEXTAREA, (await get_widgets(config, CONF_TEXTAREA))[0].obj + ) + + if is_widget_completed(textarea): + await add_textarea() + else: + CORE.add_job(add_textarea) keyboard_spec = KeyboardType() diff --git a/esphome/components/lvgl/widgets/label.py b/esphome/components/lvgl/widgets/label.py index bb5900b8c92..5ac92f2717a 100644 --- a/esphome/components/lvgl/widgets/label.py +++ b/esphome/components/lvgl/widgets/label.py @@ -35,7 +35,7 @@ class LabelType(WidgetType): if (value := config.get(CONF_TEXT)) is not None: await w.set_property(CONF_TEXT, await lv_text.process(value)) await w.set_property(CONF_LONG_MODE, config) - await w.set_property(CONF_RECOLOR, config) + await w.set_property(CONF_RECOLOR, config, processor=lv_bool) label_spec = LabelType() diff --git a/esphome/components/lvgl/widgets/line.py b/esphome/components/lvgl/widgets/line.py index a9b202163f9..3112cc28d0c 100644 --- a/esphome/components/lvgl/widgets/line.py +++ b/esphome/components/lvgl/widgets/line.py @@ -17,11 +17,6 @@ lv_point_t = cg.global_ns.struct("lv_point_t") lv_point_precise_t = cg.global_ns.struct("lv_point_precise_t") -LINE_SCHEMA = { - cv.Required(CONF_POINTS): cv.ensure_list(point_schema), -} - - async def process_coord(coord): if isinstance(coord, Lambda): return call_lambda(await cg.process_lambda(coord, [], return_type=lv_coord_t)) @@ -34,15 +29,17 @@ class LineType(WidgetType): CONF_LINE, LvType("LvLineType", parents=(LvCompound,)), (CONF_MAIN,), - LINE_SCHEMA, + schema={cv.Required(CONF_POINTS): cv.ensure_list(point_schema)}, + modify_schema={cv.Optional(CONF_POINTS): cv.ensure_list(point_schema)}, ) async def to_code(self, w: Widget, config): - points = [ - [await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])] - for p in config[CONF_POINTS] - ] - lv_add(w.var.set_points(points)) + if CONF_POINTS in config: + points = [ + [await process_coord(p[CONF_X]), await process_coord(p[CONF_Y])] + for p in config[CONF_POINTS] + ] + lv_add(w.var.set_points(points)) line_spec = LineType() diff --git a/esphome/components/lvgl/widgets/msgbox.py b/esphome/components/lvgl/widgets/msgbox.py index af27ee75531..d0e6bfa3a2b 100644 --- a/esphome/components/lvgl/widgets/msgbox.py +++ b/esphome/components/lvgl/widgets/msgbox.py @@ -33,6 +33,7 @@ from ..styles import LVStyle from ..types import LV_EVENT, lv_obj_t from . import Widget, WidgetType, add_widgets, set_obj_properties, widget_to_code from .button import button_spec, lv_button_t +from .img import CONF_IMAGE from .label import CONF_LABEL from .obj import obj_spec @@ -41,7 +42,7 @@ CONF_MSGBOX = "msgbox" OUTER_STYLE = LVStyle( "msgbox_outer", { - "bg_opa": 128, + "bg_opa": 0.5, "bg_color": "black", "border_width": 0, "pad_all": 0, @@ -119,6 +120,7 @@ async def msgbox_to_code(top_layer, conf): CONF_BUTTON, CONF_LABEL, CONF_MSGBOX, + CONF_IMAGE, *button_spec.get_uses(), ) if CONF_BUTTON_STYLE in conf: @@ -156,7 +158,7 @@ async def msgbox_to_code(top_layer, conf): with LocalVariable( "close_btn_", lv_obj_t, lv_expr.msgbox_add_close_button(msgbox) ) as close_btn: - lv_obj.remove_event_cb(close_btn, nullptr) + lv_obj.remove_event(close_btn, 0) lv_obj.add_event_cb( close_btn, await close_action.get_lambda(), @@ -170,6 +172,6 @@ async def msgbox_to_code(top_layer, conf): async def msgboxes_to_code(lv_component, config): - top_layer = lv.disp_get_layer_top(lv_component.get_disp()) + top_layer = lv_expr.disp_get_layer_top(lv_component.get_disp()) for conf in config.get(CONF_MSGBOXES, ()): await msgbox_to_code(top_layer, conf) diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index 82c43705433..df76ab6bb0d 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_SIZE, CONF_TEXT -from ..defines import CONF_MAIN, get_color_formats +from ..defines import CONF_MAIN from ..lv_validation import color, lv_color, lv_int, lv_text from ..lvcode import LocalVariable, lv from ..schemas import TEXT_SCHEMA @@ -44,7 +44,6 @@ class QrCodeType(WidgetType): return CONF_CANVAS, CONF_IMAGE async def to_code(self, w: Widget, config): - get_color_formats().add("ARGB8888") await w.set_property( CONF_LIGHT_COLOR, await lv_color.process(config.get(CONF_LIGHT_COLOR)) ) diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index 60ba664f047..7629b03e9db 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -26,7 +26,7 @@ from ..schemas import container_schema, part_schema from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties from .button import button_spec -from .buttonmatrix import buttonmatrix_spec +from .buttonmatrix import CONF_BUTTONMATRIX, buttonmatrix_spec from .obj import obj_spec CONF_TABVIEW = "tabview" @@ -73,7 +73,7 @@ class TabviewType(WidgetType): ) def get_uses(self): - return "btnmatrix", TYPE_FLEX + return CONF_BUTTONMATRIX, TYPE_FLEX async def to_code(self, w: Widget, config: dict): await w.set_property(