[lvgl] Ensure that on_value events fire on checked change (#16119)

This commit is contained in:
Clyde Stubbs
2026-05-11 09:58:18 +10:00
committed by GitHub
parent ed10fbea3e
commit 3c042e2e44
32 changed files with 532 additions and 227 deletions
+21 -17
View File
@@ -47,9 +47,15 @@ from esphome.helpers import write_file_if_changed
from esphome.writer import clean_build
from esphome.yaml_util import load_yaml
from . import defines as df, helpers, lv_validation as lvalid, widgets
from .automation import focused_widgets, layers_to_code, lvgl_update, refreshed_widgets
from .defines import CONF_ALIGN_TO_LAMBDA_ID
from . import defines as df, lv_validation as lvalid, widgets
from .automation import layers_to_code, lvgl_update
from .defines import (
CONF_ALIGN_TO_LAMBDA_ID,
get_focused_widgets,
get_lv_images_used,
get_refreshed_widgets,
set_widgets_completed,
)
from .encoders import (
ENCODERS_CONFIG,
encoders_to_code,
@@ -58,7 +64,7 @@ from .encoders import (
)
from .gradient import GRADIENT_SCHEMA, gradients_to_code
from .keypads import KEYPADS_CONFIG, keypads_to_code
from .lv_validation import lv_bool, lv_images_used
from .lv_validation import lv_bool
from .lvcode import LvContext, LvglComponent, lv_event_t_ptr, lvgl_static
from .schemas import (
DISP_BG_SCHEMA,
@@ -89,7 +95,6 @@ from .widgets import (
add_widgets,
get_screen_active,
set_obj_properties,
styles_used,
)
# Import only what we actually use directly in this file
@@ -144,7 +149,7 @@ def generate_lv_conf_h():
df.LV_DEFINES + tuple(f"LV_USE_{w.upper()}" for w in WIDGET_TYPES)
)
# Get the defines that are actually used based on the config
lv_defines = df.get_data(df.KEY_LV_DEFINES)
lv_defines = df.get_defines()
unused_defines = all_defines - set(lv_defines)
# Create the content of lv_conf.h with the used defines set to their value, and the unused defines disabled
definitions = [as_macro(m, v) for m, v in lv_defines.items()] + [
@@ -211,7 +216,7 @@ def final_validation(config_list):
buffer_frac = config[CONF_BUFFER_SIZE]
if CORE.is_esp32 and buffer_frac > 0.5 and PSRAM_DOMAIN not in global_config:
df.LOGGER.warning("buffer_size: may need to be reduced without PSRAM")
for w in focused_widgets:
for w in get_focused_widgets():
path = global_config.get_path_for_id(w)
widget_conf = global_config.get_config_for_path(path[:-1])
if (
@@ -222,7 +227,7 @@ def final_validation(config_list):
"A non adjustable arc may not be focused",
path,
)
for w in refreshed_widgets:
for w in get_refreshed_widgets():
path = global_config.get_path_for_id(w)
widget_conf = global_config.get_config_for_path(path[:-1])
if not any(isinstance(v, (Lambda, dict)) for v in widget_conf.values()):
@@ -230,7 +235,7 @@ def final_validation(config_list):
f"Widget '{w}' does not have any dynamic properties to refresh",
)
# Do per-widget type final validation for update actions
for widget_type, update_configs in df.get_data(df.KEY_UPDATED_WIDGETS).items():
for widget_type, update_configs in df.get_updated_widgets().items():
for conf in update_configs:
for id_conf in conf.get(CONF_ID, ()):
name = id_conf[CONF_ID]
@@ -279,7 +284,7 @@ async def to_code(configs):
cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[CONF_LOG_LEVEL]}"),
)
df.add_define("LV_COLOR_DEPTH", config_0[CONF_COLOR_DEPTH])
for font in helpers.lv_fonts_used:
for font in df.get_lv_fonts_used():
df.add_define(f"LV_FONT_{font.upper()}")
if config_0[CONF_COLOR_DEPTH] == 16:
@@ -294,7 +299,7 @@ async def to_code(configs):
cg.add_build_flag("-Isrc")
cg.add_global(lvgl_ns.using)
for font in helpers.esphome_fonts_used:
for font in df.get_esphome_fonts_used():
await cg.get_variable(font)
default_font = config_0[df.CONF_DEFAULT_FONT]
if not lvalid.is_lv_font(default_font):
@@ -377,8 +382,8 @@ async def to_code(configs):
await lvgl_update(lv_component, config)
await msgboxes_to_code(lv_component, config)
# await disp_update(lv_component.get_disp(), config)
# Set this directly since we are limited in how many methods can be added to the Widget class.
Widget.widgets_completed = True
# Mark all widgets as completed so awaiters of ``wait_for_widgets`` proceed.
set_widgets_completed(True)
async with LvContext():
await generate_triggers()
await generate_align_tos(configs[0])
@@ -404,9 +409,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()}")
for use in helpers.lv_uses:
styles_used = df.get_styles_used()
for use in df.get_lv_uses():
df.add_define(f"LV_USE_{use.upper()}")
cg.add_define(f"USE_LVGL_{use.upper()}")
@@ -433,7 +437,7 @@ async def to_code(configs):
} & styles_used:
lv_image_formats.add("A8")
for image_id in lv_images_used:
for image_id in get_lv_images_used():
await cg.get_variable(image_id)
metadata = get_image_metadata(image_id.id)
image_type = IMAGE_TYPE[metadata.image_type]
+10 -7
View File
@@ -25,7 +25,9 @@ from .defines import (
PARTS,
StaticCastExpression,
add_warning,
get_focused_widgets,
get_options,
get_refreshed_widgets,
)
from .lv_validation import lv_bool, lv_milliseconds
from .lvcode import (
@@ -70,9 +72,9 @@ from .widgets import (
wait_for_widgets,
)
# Record widgets that are used in a focused action here
focused_widgets = set()
refreshed_widgets = set()
# Widgets that are used in a focused/refreshed action are tracked in
# ``CORE.data`` (under the lvgl domain) so the state is cleared between
# successive compilations / unit tests via ``CORE.reset()``.
async def layers_to_code(lv_component, config):
@@ -316,7 +318,7 @@ async def resume_action_to_code(config, action_id, template_arg, args):
)
async def obj_disable_to_code(config, action_id, template_arg, args):
async def do_disable(widget: Widget):
widget.add_state(LV_STATE.DISABLED)
widget.set_state(LV_STATE.DISABLED, True)
return await action_to_code(
await get_widgets(config), do_disable, action_id, template_arg, args
@@ -328,7 +330,7 @@ async def obj_disable_to_code(config, action_id, template_arg, args):
)
async def obj_enable_to_code(config, action_id, template_arg, args):
async def do_enable(widget: Widget):
widget.clear_state(LV_STATE.DISABLED)
widget.set_state(LV_STATE.DISABLED, False)
return await action_to_code(
await get_widgets(config), do_enable, action_id, template_arg, args
@@ -361,7 +363,7 @@ async def obj_show_to_code(config, action_id, template_arg, args):
def focused_id(value):
value = cv.use_id(lv_pseudo_button_t)(value)
focused_widgets.add(value)
get_focused_widgets().add(value)
return value
@@ -446,8 +448,9 @@ async def obj_update_to_code(config, action_id, template_arg, args):
def validate_refresh_config(config):
refreshed = get_refreshed_widgets()
for w in config:
refreshed_widgets.add(w[CONF_ID])
refreshed.add(w[CONF_ID])
return config
+109 -21
View File
@@ -19,46 +19,134 @@ from esphome.cpp_generator import (
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import Expression, SafeExpType
from .helpers import requires_component
LOGGER = logging.getLogger(__name__)
lvgl_ns = cg.esphome_ns.namespace("lvgl")
DOMAIN = "lvgl"
KEY_COLOR_FORMATS = "color_formats"
KEY_ESPHOME_FONTS_USED = "esphome_fonts_used"
KEY_FOCUSED_WIDGETS = "focused_widgets"
KEY_LV_DEFINES = "lv_defines"
KEY_LV_FONTS_USED = "lv_fonts_used"
KEY_LV_IMAGES_USED = "lv_images_used"
KEY_LV_USES = "lv_uses"
KEY_NAMED_STYLES = "named_styles"
KEY_REFRESHED_WIDGETS = "refreshed_widgets"
KEY_REMAPPED_USES = "remapped_uses"
KEY_STYLES_USED = "styles_used"
KEY_THEME_WIDGET_MAP = "theme_widget_map"
KEY_UPDATED_WIDGETS = "updated_widgets"
KEY_WIDGET_MAP = "widget_map"
KEY_WIDGETS_COMPLETED = "widgets_completed"
KEY_OPTIONS = "options"
KEY_WARNINGS = "warnings"
# Initial set of LVGL features that are always enabled.
_INITIAL_LV_USES = frozenset(
{
"USER_DATA",
"LOG",
"STYLE",
"FONT_PLACEHOLDER",
"THEME_DEFAULT",
}
)
def get_data(key, default=None):
# These collections accumulate state across a single compilation run. They
# are stored under ``CORE.data`` (which ``CORE.reset()`` clears between runs)
# rather than as module-level globals, otherwise they would leak between
# successive compilations / unit tests.
def _get_data(key: str, default: Any) -> Any:
"""
Get a data structure from the global data store by key
:param key: A key for the data
:param default: The default data - the default is an empty dict
:param default: The default data
:return:
"""
return CORE.data.setdefault(DOMAIN, {}).setdefault(
key, {} if default is None else default
)
return CORE.data.setdefault(DOMAIN, {}).setdefault(key, default)
def get_warnings():
return get_data(KEY_WARNINGS, set())
def get_lv_images_used() -> set[ID]:
return _get_data(KEY_LV_IMAGES_USED, set())
def get_remapped_uses():
return get_data(KEY_REMAPPED_USES, set())
def get_lv_uses() -> set[str]:
return _get_data(KEY_LV_USES, set(_INITIAL_LV_USES))
def add_warning(msg: str):
def get_lv_fonts_used() -> set[str]:
return _get_data(KEY_LV_FONTS_USED, set())
def get_esphome_fonts_used() -> set[ID]:
return _get_data(KEY_ESPHOME_FONTS_USED, set())
def add_lv_use(*names: str) -> None:
uses = get_lv_uses()
for name in names:
uses.add(name)
def get_warnings() -> set[str]:
return _get_data(KEY_WARNINGS, set())
def get_remapped_uses() -> set[str]:
return _get_data(KEY_REMAPPED_USES, set())
def add_warning(msg: str) -> None:
get_warnings().add(msg)
def get_options():
return get_data(KEY_OPTIONS)
def get_options() -> dict[str, Any]:
return _get_data(KEY_OPTIONS, {})
def get_defines() -> dict[str, str]:
return _get_data(KEY_LV_DEFINES, {})
def get_updated_widgets() -> dict:
return _get_data(KEY_UPDATED_WIDGETS, {})
def get_theme_widget_map() -> dict[str, Any]:
return _get_data(KEY_THEME_WIDGET_MAP, {})
def get_styles_used() -> set[str]:
return _get_data(KEY_STYLES_USED, set())
def get_widget_map() -> dict[str, Any]:
return _get_data(KEY_WIDGET_MAP, {})
def get_widgets_completed() -> bool:
# ``[value]`` rather than the bare value so that we can mutate the
# entry in place; ``CORE.data`` is reset for us between runs.
return _get_data(KEY_WIDGETS_COMPLETED, [False])[0]
def set_widgets_completed(value: bool) -> None:
_get_data(KEY_WIDGETS_COMPLETED, [False])[0] = value
def is_widget_completed(name: ID) -> bool:
return name in get_widget_map()
def get_focused_widgets() -> set:
return _get_data(KEY_FOCUSED_WIDGETS, set())
def get_refreshed_widgets() -> set:
return _get_data(KEY_REFRESHED_WIDGETS, set())
class StaticCastExpression(Expression):
@@ -72,8 +160,8 @@ class StaticCastExpression(Expression):
return f"static_cast<{self.type}>({self.exp})"
def add_define(macro, value="1"):
lv_defines = get_data(KEY_LV_DEFINES)
def add_define(macro: str, value="1"):
lv_defines = get_defines()
value = str(value)
if lv_defines.setdefault(macro, value) != value:
LOGGER.error(
@@ -82,8 +170,8 @@ def add_define(macro, value="1"):
lv_defines[macro] = value
def is_defined(macro):
return macro in get_data(KEY_LV_DEFINES)
def is_defined(macro) -> bool:
return macro in get_defines()
def literal(arg) -> MockObj:
@@ -96,7 +184,7 @@ def addr(arg) -> MockObj:
return MockObj(f"&{arg}")
def call_lambda(lamb: LambdaExpression):
def call_lambda(lamb: LambdaExpression) -> Expression:
"""
Given a lambda, either reduce to a simple expression or call it, possibly with parameters
from the surrounding context
@@ -135,7 +223,7 @@ class LValidator:
def __call__(self, value):
if self.requires:
value = requires_component(self.requires)(value)
value = cv.requires_component(self.requires)(value)
if isinstance(value, cv.Lambda):
return cv.returning_lambda(value)
return self.validator(value)
@@ -196,7 +284,7 @@ class LvConstant(LValidator):
cv.ensure_list(self.one_of), cg.uint32, retmapper=self.mapper
)
def mapper(self, value):
def mapper(self, value) -> Any:
if not isinstance(value, list):
value = [value]
value = [
+4 -3
View File
@@ -13,8 +13,8 @@ from .defines import (
CONF_LONG_PRESS_REPEAT_TIME,
CONF_LONG_PRESS_TIME,
CONF_RIGHT_BUTTON,
add_lv_use,
)
from .helpers import lvgl_components_required, requires_component
from .lvcode import lv, lv_add, lv_assign, lv_expr, lv_Pvariable
from .schemas import ENCODER_SCHEMA
from .types import lv_group_t, lv_indev_type_t, lv_key_t
@@ -26,7 +26,8 @@ ENCODERS_CONFIG = cv.ensure_list(
cv.Required(CONF_ENTER_BUTTON): cv.use_id(BinarySensor),
cv.Required(CONF_SENSOR): cv.Any(
cv.All(
cv.use_id(RotaryEncoderSensor), requires_component("rotary_encoder")
cv.use_id(RotaryEncoderSensor),
cv.requires_component("rotary_encoder"),
),
cv.Schema(
{
@@ -48,7 +49,7 @@ def get_default_group(config):
async def encoders_to_code(var, config, default_group):
for enc_conf in config[CONF_ENCODERS]:
lvgl_components_required.add("KEY_LISTENER")
add_lv_use("KEY_LISTENER", "ROTARY_ENCODER")
lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds
lprt = enc_conf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds
listener = cg.new_Pvariable(
+8 -2
View File
@@ -12,8 +12,14 @@ from esphome.const import (
from esphome.core import ID
from esphome.cpp_generator import MockObj
from .defines import CONF_GRADIENTS, CONF_OPA, LV_DITHER, add_define, add_warning
from .helpers import add_lv_use
from .defines import (
CONF_GRADIENTS,
CONF_OPA,
LV_DITHER,
add_define,
add_lv_use,
add_warning,
)
from .lv_validation import lv_color, lv_percentage, opacity
from .lvcode import lv
from .types import lv_color_t, lv_gradient_t, lv_opa_t
-25
View File
@@ -5,23 +5,6 @@ from esphome.const import CONF_ARGS, CONF_FORMAT
CONF_IF_NAN = "if_nan"
lv_uses = {
"USER_DATA",
"LOG",
"STYLE",
"FONT_PLACEHOLDER",
"THEME_DEFAULT",
}
def add_lv_use(*names):
for name in names:
lv_uses.add(name)
lv_fonts_used = set()
esphome_fonts_used = set()
lvgl_components_required = set()
# noqa
f_regex = re.compile(
@@ -66,11 +49,3 @@ def validate_printf(value):
"Use of 'if_nan' requires a single valid printf-pattern of type %f"
)
return value
def requires_component(comp):
def validator(value):
lvgl_components_required.add(comp)
return cv.requires_component(comp)(value)
return validator
+2 -2
View File
@@ -9,9 +9,9 @@ from .defines import (
CONF_KEYPADS,
CONF_LONG_PRESS_REPEAT_TIME,
CONF_LONG_PRESS_TIME,
add_lv_use,
literal,
)
from .helpers import lvgl_components_required
from .lvcode import lv, lv_assign, lv_expr, lv_Pvariable
from .schemas import ENCODER_SCHEMA
from .types import lv_group_t, lv_indev_type_t
@@ -52,7 +52,7 @@ KEYPADS_CONFIG = cv.ensure_list(
async def keypads_to_code(var, config, default_group):
for enc_conf in config[CONF_KEYPADS]:
lvgl_components_required.add("KEY_LISTENER")
add_lv_use("KEY_LISTENER")
lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds
lprt = enc_conf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds
listener = cg.new_Pvariable(
+1 -1
View File
@@ -37,7 +37,7 @@ class LVLight : public light::LightOutput {
void set_value_(lv_color_t value) {
lv_led_set_color(this->obj_, value);
lv_led_on(this->obj_);
lv_obj_send_event(this->obj_, lv_api_event, nullptr);
lv_obj_send_event(this->obj_, lv_update_event, nullptr);
}
lv_obj_t *obj_{};
optional<lv_color_t> initial_value_{};
+11 -16
View File
@@ -31,16 +31,14 @@ from .defines import (
LValidator,
LvConstant,
StaticCastExpression,
add_lv_use,
call_lambda,
get_esphome_fonts_used,
get_lv_fonts_used,
get_lv_images_used,
literal,
)
from .helpers import (
CONF_IF_NAN,
add_lv_use,
esphome_fonts_used,
lv_fonts_used,
requires_component,
)
from .helpers import CONF_IF_NAN
from .types import lv_coord_t, lv_gradient_t, lv_opa_t
LV_OPA = LvConstant("LV_OPA_", "TRANSP", "COVER")
@@ -370,14 +368,11 @@ def stop_value(value):
return cv.int_range(0, 255)(value)
lv_images_used = set()
def image_validator(value):
value = requires_component("image")(value)
value = cv.requires_component("image")(value)
value = cv.use_id(Image_)(value)
lv_images_used.add(value)
add_lv_use("img", "label")
get_lv_images_used().add(value)
add_lv_use("label")
return value
@@ -496,7 +491,7 @@ class LvFont(LValidator):
def __init__(self):
def lv_builtin_font(value):
fontval = cv.one_of(*LV_FONTS, lower=True)(value)
lv_fonts_used.add(fontval)
get_lv_fonts_used().add(fontval)
return fontval
def validator(value):
@@ -506,8 +501,8 @@ class LvFont(LValidator):
return lv_builtin_font(value)
add_lv_use("font")
fontval = cv.use_id(Font)(value)
esphome_fonts_used.add(fontval)
return requires_component("font")(fontval)
get_esphome_fonts_used().add(fontval)
return cv.requires_component("font")(fontval)
# Use font::Font* as return type for lambdas returning ESPHome fonts
# The inline overloads in lvgl_esphome.h handle conversion to lv_font_t*
+1 -2
View File
@@ -29,10 +29,9 @@ LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent)
LVGL_COMP_ARG = [(LvglComponent.operator("ptr"), LVGL_COMP)]
lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr")
EVENT_ARG = [(lv_event_t_ptr, "event")]
# Two custom events; API_EVENT is fired when an entity is updated remotely by an API interaction;
# One custom event;
# UPDATE_EVENT is fired when an entity is programmatically updated locally.
# VALUE_CHANGED is the event generated by LVGL when an entity's value changes through user interaction.
API_EVENT = literal("lvgl::lv_api_event")
UPDATE_EVENT = literal("lvgl::lv_update_event")
+1 -3
View File
@@ -147,7 +147,6 @@ void LvglComponent::render_start_cb(lv_event_t *event) {
comp->draw_start_();
}
lv_event_code_t lv_api_event; // NOLINT
lv_event_code_t lv_update_event; // NOLINT
void LvglComponent::dump_config() {
ESP_LOGCONFIG(TAG,
@@ -194,7 +193,6 @@ void LvglComponent::esphome_lvgl_init() {
LV_GLOBAL_DEFAULT()->font_draw_buf_handlers.buf_free_cb = lv_free_core;
lv_tick_set_cb([] { return millis(); });
lv_update_event = static_cast<lv_event_code_t>(lv_event_register_id());
lv_api_event = static_cast<lv_event_code_t>(lv_event_register_id());
}
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) {
@@ -547,7 +545,7 @@ void LvSelectable::set_selected_text(const std::string &text, lv_anim_enable_t a
auto index = std::find(this->options_.begin(), this->options_.end(), text);
if (index != this->options_.end()) {
this->set_selected_index(index - this->options_.begin(), anim);
lv_obj_send_event(this->obj, lv_api_event, nullptr);
lv_obj_send_event(this->obj, lv_update_event, nullptr);
}
}
+32 -1
View File
@@ -50,7 +50,6 @@ using lv_color_data = uint16_t;
using lv_color_data = uint32_t;
#endif
extern lv_event_code_t lv_api_event; // NOLINT
extern lv_event_code_t lv_update_event; // NOLINT
extern std::string lv_event_code_name_for(lv_event_t *event);
@@ -227,11 +226,43 @@ class LvglComponent : public PollingComponent {
* Initialize the LVGL library and register custom events.
*/
static void esphome_lvgl_init();
// Convenience overloads for adding a callback for one or more events
static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event);
static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2);
static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2,
lv_event_code_t event3);
// change the state of a widget and fire an event if changed (only needed for CHECKED)
static void lv_obj_set_state_value(lv_obj_t *obj, lv_state_t state, bool value) {
if (value != lv_obj_has_state(obj, state)) {
if (value) {
lv_obj_add_state(obj, state);
} else {
lv_obj_remove_state(obj, state);
}
if (state == LV_STATE_CHECKED)
lv_obj_send_event(obj, lv_update_event, nullptr);
}
}
// change the state of a buttonmatrix button and fire an event if changed (only needed for CHECKED)
#ifdef USE_LVGL_BUTTONMATRIX
static void lv_buttonmatrix_set_button_ctrl_value(lv_obj_t *obj, uint32_t index, lv_buttonmatrix_ctrl_t ctrl,
bool value) {
if (value != lv_buttonmatrix_has_button_ctrl(obj, index, ctrl)) {
if (value) {
lv_buttonmatrix_set_button_ctrl(obj, index, ctrl);
} else {
lv_buttonmatrix_clear_button_ctrl(obj, index, ctrl);
}
if (ctrl == LV_BUTTONMATRIX_CTRL_CHECKED)
lv_obj_send_event(obj, lv_update_event, nullptr);
}
}
#endif
void add_page(LvPageType *page);
void show_page(size_t index, lv_screen_load_anim_t anim, uint32_t time);
void show_next_page(lv_screen_load_anim_t anim, uint32_t time);
+1 -2
View File
@@ -7,7 +7,6 @@ from esphome.cpp_generator import MockObj
from ..defines import CONF_ANIMATED, CONF_UPDATE_ON_RELEASE, CONF_WIDGET
from ..lv_validation import animated
from ..lvcode import (
API_EVENT,
EVENT_ARG,
UPDATE_EVENT,
LambdaContext,
@@ -40,7 +39,7 @@ async def to_code(config):
await widget.set_property(
"value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED]
)
lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr)
lv_obj.send_event(widget.obj, UPDATE_EVENT, cg.nullptr)
event_code = (
LV_EVENT.VALUE_CHANGED
if not config[CONF_UPDATE_ON_RELEASE]
+4 -4
View File
@@ -33,7 +33,7 @@ from .defines import (
get_remapped_uses,
is_press_event,
)
from .helpers import CONF_IF_NAN, requires_component, validate_printf
from .helpers import CONF_IF_NAN, validate_printf
from .layout import (
FLEX_OBJ_SCHEMA,
GRID_CELL_SCHEMA,
@@ -112,7 +112,7 @@ PRESS_TIME = cv.All(
ENCODER_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.All(
cv.declare_id(LVEncoderListener), requires_component("binary_sensor")
cv.declare_id(LVEncoderListener), cv.requires_component("binary_sensor")
),
cv.Optional(CONF_GROUP): cv.declare_id(lv_group_t),
cv.Optional(df.CONF_INITIAL_FOCUS): cv.All(
@@ -406,7 +406,7 @@ def _update_widget(widget_type: WidgetType) -> Callable[[dict], dict]:
"""
def validator(value: dict) -> dict:
df.get_data(df.KEY_UPDATED_WIDGETS).setdefault(widget_type, []).append(value)
df.get_updated_widgets().setdefault(widget_type, []).append(value)
return value
return validator
@@ -573,7 +573,7 @@ def any_widget_schema(extras=None):
container_validator = container_schema(widget_type, extras=extras)
if required := widget_type.required_component:
container_validator = cv.All(
container_validator, requires_component(required)
container_validator, cv.requires_component(required)
)
# Apply custom validation
path = [key] if is_dict else [index, key]
@@ -3,7 +3,6 @@ import esphome.config_validation as cv
from ..defines import CONF_WIDGET
from ..lvcode import (
API_EVENT,
EVENT_ARG,
UPDATE_EVENT,
LambdaContext,
@@ -35,7 +34,6 @@ async def to_code(config):
widget.obj,
await lamb.get_lambda(),
LV_EVENT.VALUE_CHANGED,
API_EVENT,
UPDATE_EVENT,
)
)
+10 -4
View File
@@ -4,12 +4,18 @@ 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, LValidator, literal
from .helpers import add_lv_use
from .defines import (
CONF_STYLE_DEFINITIONS,
CONF_THEME,
LValidator,
add_lv_use,
get_theme_widget_map,
literal,
)
from .lvcode import LambdaContext, lv
from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, WIDGET_TYPES, remap_property
from .types import ObjUpdateAction, lv_style_t
from .widgets import collect_parts, theme_widget_map, wait_for_widgets
from .widgets import collect_parts, wait_for_widgets
def has_style_props(config) -> bool:
@@ -97,4 +103,4 @@ async def theme_to_code(config):
)
for state, props in states.items()
}
theme_widget_map[w_name] = styles
get_theme_widget_map()[w_name] = styles
+1 -8
View File
@@ -7,14 +7,11 @@ from esphome.cpp_types import Component
from ..defines import CONF_WIDGET, literal
from ..lvcode import (
API_EVENT,
EVENT_ARG,
UPDATE_EVENT,
LambdaContext,
LvConditional,
LvContext,
lv_add,
lv_obj,
lvgl_static,
)
from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns
@@ -35,11 +32,7 @@ async def to_code(config):
switch_id = MockObj(config[CONF_ID], "->")
v = literal("v")
async with LambdaContext([(cg.bool_, "v")]) as control:
with LvConditional(v) as cond:
widget.add_state(LV_STATE.CHECKED)
cond.else_()
widget.clear_state(LV_STATE.CHECKED)
lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr)
widget.set_state(LV_STATE.CHECKED, literal("v"))
control.add(switch_id.publish_state(v))
switch = cg.new_Pvariable(config[CONF_ID], await control.get_lambda())
await cg.register_component(switch, config)
+1 -2
View File
@@ -5,7 +5,6 @@ import esphome.config_validation as cv
from ..defines import CONF_WIDGET
from ..lvcode import (
API_EVENT,
EVENT_ARG,
UPDATE_EVENT,
LambdaContext,
@@ -33,7 +32,7 @@ async def to_code(config):
await wait_for_widgets()
async with LambdaContext([(cg.std_string, "text_value")]) as control:
await widget.set_property("text", "text_value.c_str()")
lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr)
lv_obj.send_event(widget.obj, UPDATE_EVENT, cg.nullptr)
control.add(textvar.publish_state(widget.get_value()))
async with LambdaContext(EVENT_ARG) as lamb:
lv_add(textvar.publish_state(widget.get_value()))
@@ -6,14 +6,7 @@ from esphome.components.text_sensor import (
import esphome.config_validation as cv
from ..defines import CONF_WIDGET
from ..lvcode import (
API_EVENT,
EVENT_ARG,
UPDATE_EVENT,
LambdaContext,
LvContext,
lvgl_static,
)
from ..lvcode import EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext, lvgl_static
from ..types import LV_EVENT, LvText
from ..widgets import get_widgets, wait_for_widgets
@@ -37,7 +30,6 @@ async def to_code(config):
widget.obj,
await pressed_ctx.get_lambda(),
LV_EVENT.VALUE_CHANGED,
API_EVENT,
UPDATE_EVENT,
)
)
+5 -3
View File
@@ -8,15 +8,17 @@ from .defines import (
CONF_LONG_PRESS_REPEAT_TIME,
CONF_LONG_PRESS_TIME,
CONF_TOUCHSCREENS,
add_lv_use,
)
from .helpers import lvgl_components_required
from .schemas import PRESS_TIME
from .types import LVTouchListener
CONF_TOUCHSCREEN = "touchscreen"
TOUCHSCREENS_CONFIG = cv.maybe_simple_value(
{
cv.Required(CONF_TOUCHSCREEN_ID): cv.use_id(Touchscreen),
cv.Required(CONF_TOUCHSCREEN_ID): cv.All(
cv.use_id(Touchscreen), cv.requires_component(CONF_TOUCHSCREEN)
),
cv.Optional(CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME,
cv.Optional(CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME,
cv.GenerateID(): cv.declare_id(LVTouchListener),
@@ -34,7 +36,7 @@ def touchscreen_schema(config):
async def touchscreens_to_code(lv_component, config):
for tconf in config[CONF_TOUCHSCREENS]:
lvgl_components_required.add(CONF_TOUCHSCREEN)
add_lv_use(CONF_TOUCHSCREEN)
touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID])
lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds
lprt = tconf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds
+4 -4
View File
@@ -24,11 +24,11 @@ from .defines import (
LV_SCREEN_EVENT_MAP,
LV_SCREEN_EVENT_TRIGGERS,
SWIPE_TRIGGERS,
get_widget_map,
is_press_event,
literal,
)
from .lvcode import (
API_EVENT,
EVENT_ARG,
UPDATE_EVENT,
LambdaContext,
@@ -39,7 +39,7 @@ from .lvcode import (
lvgl_static,
)
from .types import LV_EVENT, lv_point_t
from .widgets import LvScrActType, get_screen_active, widget_map
from .widgets import LvScrActType, get_screen_active
async def add_on_boot_triggers(triggers):
@@ -58,7 +58,7 @@ async def generate_triggers():
all_triggers = (
LV_EVENT_TRIGGERS + LV_DISPLAY_EVENT_TRIGGERS + LV_SCREEN_EVENT_TRIGGERS
)
for w in widget_map.values():
for w in get_widget_map().values():
config = w.config
if isinstance(w.type, LvScrActType):
w = get_screen_active(w.var)
@@ -89,7 +89,6 @@ async def generate_triggers():
conf,
w,
LV_EVENT.VALUE_CHANGED,
API_EVENT,
UPDATE_EVENT,
)
@@ -104,6 +103,7 @@ async def generate_align_tos(config: dict):
:param config:
:return:
"""
widget_map = get_widget_map()
align_tos = tuple(
w for w in widget_map.values() if w.config and CONF_ALIGN_TO in w.config
)
+28 -57
View File
@@ -15,6 +15,7 @@ from esphome.const import (
from esphome.core import ID, EsphomeError, TimePeriod
from esphome.coroutine import FakeAwaitable
from esphome.cpp_generator import MockObj
from esphome.types import Expression
from ..defines import (
CONF_FLEX_ALIGN_CROSS,
@@ -38,11 +39,15 @@ from ..defines import (
TYPE_FLEX,
TYPE_GRID,
LValidator,
add_lv_use,
call_lambda,
get_styles_used,
get_theme_widget_map,
get_widget_map,
get_widgets_completed,
join_enums,
literal,
)
from ..helpers import add_lv_use
from ..lvcode import (
LvConditional,
add_line_marks,
@@ -52,6 +57,7 @@ from ..lvcode import (
lv_expr,
lv_obj,
lv_Pvariable,
lvgl_static,
)
from ..types import (
LV_STATE,
@@ -65,9 +71,6 @@ from ..types import (
EVENT_LAMB = "event_lamb__"
theme_widget_map = {}
styles_used = set()
class WidgetType:
"""
@@ -157,7 +160,7 @@ class WidgetType:
await self.on_create(var, config)
w = Widget.create(wid, var, self, config)
if theme := theme_widget_map.get(self.name):
if theme := get_theme_widget_map().get(self.name):
for part, states in theme.items():
part = "LV_PART_" + part.upper()
for state, style in states.items():
@@ -240,8 +243,6 @@ class Widget:
This class has a lot of methods. Adding any more runs foul of lint checks ("too many public methods").
"""
widgets_completed = False
def __init__(self, var, wtype: WidgetType, config: dict = None):
self.var = var
self.type = wtype
@@ -262,21 +263,14 @@ class Widget:
@staticmethod
def create(name, var, wtype: WidgetType, config: dict = None):
w = Widget(var, wtype, config)
widget_map[name] = w
get_widget_map()[name] = w
return w
def add_state(self, state):
if "|" in state:
state = f"(lv_state_t)({state})"
return lv_obj.add_state(self.obj, literal(state))
def set_state(self, state: MockObj, value: bool | Expression):
lv_add(lvgl_static.lv_obj_set_state_value(self.obj, state, value))
def clear_state(self, state):
if "|" in state:
state = f"(lv_state_t)({state})"
return lv_obj.remove_state(self.obj, literal(state))
def has_state(self, state):
return (lv_expr.obj_get_state(self.obj) & literal(state)) != 0
def has_state(self, state: MockObj):
return lv_expr.obj_has_state(self.obj, state)
def is_pressed(self):
return self.has_state(LV_STATE.PRESSED)
@@ -346,10 +340,10 @@ class Widget:
ltype = ltype or self.__type_base()
return cg.RawExpression(f"lv_{ltype}_get_{prop}({self.obj})")
def set_style(self, prop, value, state=LV_STATE.DEFAULT):
def set_style(self, prop: str, value, state=LV_STATE.DEFAULT):
if value is None:
return
styles_used.add(prop)
get_styles_used().add(prop)
if isinstance(value, str):
value = literal(value)
lv.call(f"obj_set_style_{prop}", self.obj, value, state)
@@ -403,14 +397,6 @@ class Widget:
return self.type.get_scale(self.config)
# Map of widgets to their config, used for trigger generation
widget_map: dict[ID, Widget] = {}
def is_widget_completed(name: ID) -> bool:
return name in widget_map
class LvScrActType(WidgetType):
"""
A "widget" representing the active screen.
@@ -433,10 +419,11 @@ def get_widget_generator(wid):
:param wid:
:return:
"""
widget_map = get_widget_map()
while True:
if obj := widget_map.get(wid):
return obj
if Widget.widgets_completed:
if get_widgets_completed():
raise Invalid(
f"Widget {wid} not found, yet all widgets should be defined by now"
)
@@ -444,20 +431,20 @@ def get_widget_generator(wid):
async def get_widget_(wid):
if obj := widget_map.get(wid):
if obj := get_widget_map().get(wid):
return obj
return await FakeAwaitable(get_widget_generator(wid))
def widgets_wait_generator():
while True:
if Widget.widgets_completed:
if get_widgets_completed():
return
yield
async def wait_for_widgets():
if Widget.widgets_completed:
if get_widgets_completed():
return
await FakeAwaitable(widgets_wait_generator())
@@ -608,30 +595,14 @@ async def set_obj_properties(w: Widget, config):
cond.else_()
w.clear_flag(flag)
if states := config.get(CONF_STATE):
adds = set()
clears = set()
lambs = {}
for key, value in states.items():
if isinstance(value, cv.Lambda):
lambs[key] = value
elif value:
adds.add(key)
else:
clears.add(key)
if adds:
adds = join_enums(adds, "LV_STATE_")
w.add_state(adds)
if clears:
clears = join_enums(clears, "LV_STATE_")
w.clear_state(clears)
for key, value in lambs.items():
lamb = await cg.process_lambda(value, [], capture="=", return_type=cg.bool_)
state = f"LV_STATE_{key.upper()}"
with LvConditional(call_lambda(lamb)) as cond:
w.add_state(state)
cond.else_()
w.clear_state(state)
for key, value in config.get(CONF_STATE, {}).items():
if isinstance(value, cv.Lambda):
value = call_lambda(
await cg.process_lambda(value, [], capture="=", return_type=cg.bool_)
)
state = getattr(LV_STATE, key.upper())
w.set_state(state, value)
for property in OBJ_PROPERTIES:
await w.set_property(property, config, lv_name="obj")
+1 -4
View File
@@ -4,7 +4,6 @@ from esphome.const import CONF_DURATION, CONF_ID
from ..automation import action_to_code
from ..defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC
from ..helpers import lvgl_components_required
from ..lv_validation import lv_image_list, lv_milliseconds
from ..lvcode import lv
from ..types import LvType, ObjUpdateAction
@@ -55,8 +54,6 @@ class AnimimgType(WidgetType):
)
async def to_code(self, w: Widget, config):
lvgl_components_required.add(CONF_IMAGE)
lvgl_components_required.add(CONF_ANIMIMG)
if srcs := config.get(CONF_SRC):
srcs = await lv_image_list.process(srcs)
lv.animimg_set_src(w.obj, srcs)
@@ -68,7 +65,7 @@ class AnimimgType(WidgetType):
lv.animimg_start(w.obj)
def get_uses(self):
return "img", CONF_IMAGE, CONF_LABEL
return CONF_IMAGE, CONF_LABEL
animimg_spec = AnimimgType()
+1 -2
View File
@@ -2,8 +2,7 @@ from esphome import config_validation as cv
from esphome.const import CONF_BUTTON, CONF_TEXT
from esphome.cpp_generator import MockObj
from ..defines import CONF_MAIN, CONF_WIDGETS
from ..helpers import add_lv_use
from ..defines import CONF_MAIN, CONF_WIDGETS, add_lv_use
from ..lv_validation import lv_text
from ..lvcode import lv, lv_expr
from ..schemas import TEXT_SCHEMA
+12 -12
View File
@@ -5,6 +5,7 @@ from esphome.components.key_provider import KeyProvider
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH
from esphome.cpp_generator import MockObj
from esphome.types import Expression
from ..automation import action_to_code
from ..defines import (
@@ -17,10 +18,10 @@ from ..defines import (
CONF_PAD_COLUMN,
CONF_PAD_ROW,
CONF_SELECTED,
get_widget_map,
)
from ..helpers import lvgl_components_required
from ..lv_validation import key_code, lv_bool, padding
from ..lvcode import lv, lv_add, lv_expr
from ..lvcode import lv, lv_add, lv_expr, lvgl_static
from ..schemas import automation_schema
from ..types import (
LV_BTNMATRIX_CTRL,
@@ -32,7 +33,7 @@ from ..types import (
char_ptr,
lv_pseudo_button_t,
)
from . import Widget, WidgetType, get_widgets, widget_map
from . import Widget, WidgetType, get_widgets
from .button import lv_button_t
CONF_BUTTONMATRIX = "buttonmatrix"
@@ -98,7 +99,7 @@ class MatrixButton(Widget):
@staticmethod
def create_button(id, parent, config: dict, index):
w = MatrixButton(id, parent, config, index)
widget_map[id] = w
get_widget_map()[id] = w
return w
def __init__(self, id, parent: Widget, config, index):
@@ -120,13 +121,13 @@ class MatrixButton(Widget):
state = self.map_ctrls(state)
return lv_expr.buttonmatrix_has_button_ctrl(self.obj, self.index, state)
def add_state(self, state):
state = self.map_ctrls(state)
return lv.buttonmatrix_set_button_ctrl(self.obj, self.index, state)
def clear_state(self, state):
state = self.map_ctrls(state)
return lv.buttonmatrix_clear_button_ctrl(self.obj, self.index, state)
def set_state(self, state: MockObj, value: bool | Expression):
ctrl = self.map_ctrls(state)
lv_add(
lvgl_static.lv_buttonmatrix_set_button_ctrl_value(
self.obj, self.index, ctrl, value
)
)
def is_pressed(self):
return self.is_selected() & self.parent.has_state(LV_STATE.PRESSED)
@@ -191,7 +192,6 @@ class ButtonMatrixType(WidgetType):
)
async def to_code(self, w: Widget, config):
lvgl_components_required.add("BUTTONMATRIX")
if CONF_ROWS not in config:
return
text_list, ctrl_list, width_list, key_list = await get_button_data(
+1 -3
View File
@@ -14,7 +14,6 @@ from ..defines import (
DIRECTIONS,
literal,
)
from ..helpers import lvgl_components_required
from ..lv_validation import lv_int, lv_text, option_string
from ..lvcode import LocalVariable, lv, lv_add, lv_expr
from ..schemas import part_schema
@@ -95,7 +94,6 @@ class DropdownType(WidgetType):
)
async def to_code(self, w: Widget, config):
lvgl_components_required.add(CONF_DROPDOWN)
if options := config.get(CONF_OPTIONS):
lv_add(w.var.set_options(options))
if symbol := config.get(CONF_SYMBOL):
@@ -116,7 +114,7 @@ class DropdownType(WidgetType):
await set_obj_properties(dwid, dlist)
def get_uses(self):
return (CONF_LABEL,)
return CONF_LABEL, CONF_DROPDOWN
dropdown_spec = DropdownType()
+9 -5
View File
@@ -5,10 +5,15 @@ 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 lvgl_components_required
from ..defines import (
CONF_MAIN,
KEYBOARD_MODES,
add_lv_use,
is_widget_completed,
literal,
)
from ..types import LvCompound, LvType
from . import Widget, WidgetType, get_widgets, is_widget_completed
from . import Widget, WidgetType, get_widgets
from .buttonmatrix import CONF_BUTTONMATRIX
from .textarea import CONF_TEXTAREA, lv_textarea_t
@@ -47,8 +52,7 @@ class KeyboardType(WidgetType):
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("KEY_LISTENER")
if mode := config.get(CONF_MODE):
await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode))
if textarea := config.get(CONF_TEXTAREA):
+2 -3
View File
@@ -41,10 +41,10 @@ from ..defines import (
LV_OBJ_FLAG,
LV_PART,
LV_SCALE_MODE,
add_lv_use,
get_remapped_uses,
get_warnings,
)
from ..helpers import add_lv_use
from ..lv_validation import (
LV_OPA,
LV_RADIUS,
@@ -61,7 +61,6 @@ from ..lv_validation import (
padding,
pixels,
pixels_or_percent,
requires_component,
size,
)
from ..lvcode import LambdaContext, LocalVariable, lv, lv_add, lv_expr, lv_obj
@@ -214,7 +213,7 @@ INDICATOR_SCHEMA = cv.Schema(
cv.GenerateID(CONF_IMAGE_ID): cv.declare_id(lv_image_t),
}
),
requires_component("image"),
cv.requires_component("image"),
),
cv.Exclusive(CONF_ARC, CONF_INDICATORS): INDICATOR_ARC_SCHEMA.extend(
{
+1 -1
View File
@@ -16,10 +16,10 @@ from ..defines import (
CONF_TITLE,
LV_OBJ_FLAG,
TYPE_FLEX,
add_lv_use,
add_warning,
literal,
)
from ..helpers import add_lv_use
from ..lv_validation import lv_bool, lv_image, lv_text, pixels_or_percent
from ..lvcode import EVENT_ARG, LambdaContext, LocalVariable, lv, lv_expr, lv_obj
from ..schemas import (
@@ -11,7 +11,6 @@ from ..defines import (
ROLLER_MODES,
literal,
)
from ..helpers import lvgl_components_required
from ..lv_validation import animated, lv_int, lv_text, option_string
from ..lvcode import lv_add
from ..types import LvSelect
@@ -55,7 +54,6 @@ class RollerType(WidgetType):
)
async def to_code(self, w, config):
lvgl_components_required.add(CONF_ROLLER)
if mode := config.get(CONF_MODE):
mode = await ROLLER_MODES.process(mode)
lv_add(w.var.set_mode(mode))
@@ -0,0 +1,83 @@
esphome:
name: test
esp32:
board: esp32dev
framework:
type: esp-idf
spi:
- id: spi_bus
clk_pin: GPIO18
mosi_pin: GPIO23
display:
- platform: mipi_spi
spi_id: spi_bus
model: st7789v
id: tft_display
dimensions:
width: 240
height: 320
cs_pin: GPIO22
dc_pin: GPIO21
auto_clear_enabled: false
invert_colors: false
update_interval: never
lvgl:
id: lvgl_id
displays: tft_display
pages:
- id: main_page
widgets:
# Widget with multiple static states; one true, one false.
- button:
id: btn_static
state:
checked: true
disabled: false
# Widget with a templated (lambda) state.
- button:
id: btn_lambda
state:
pressed: !lambda return true;
# Button referenced by enable/disable actions; the on_click handler
# exercises both branches of the obj_disable/obj_enable code path.
- button:
id: btn_actions
on_click:
- lvgl.widget.disable: btn_actions
- lvgl.widget.enable: btn_actions
# Button matrix with two buttons; matrix_btn_a is targeted by
# lvgl.widget.disable/enable actions to exercise the
# MatrixButton.set_state code path.
- buttonmatrix:
id: matrix
rows:
- buttons:
- id: matrix_btn_a
text: A
control:
checkable: true
- id: matrix_btn_b
text: B
control:
checkable: true
on_click:
- lvgl.widget.disable: matrix_btn_a
- lvgl.widget.enable: matrix_btn_a
# Switch derived from an LVGL switch widget exercises
# set_state(LV_STATE.CHECKED, v) inside the control lambda.
- switch:
id: switch_widget
switch:
- platform: lvgl
id: lvgl_switch
name: LVGL Switch
widget: switch_widget
@@ -0,0 +1,167 @@
"""Tests for LVGL widget state code generation.
These tests cover the change from the old ``add_state``/``clear_state`` helpers
on :class:`Widget` (and on :class:`MatrixButton`) to a single ``set_state``
method that delegates to the new C++ helpers
``LvglComponent::lv_obj_set_state_value`` and
``LvglComponent::lv_buttonmatrix_set_button_ctrl_value``.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from esphome.__main__ import generate_cpp_contents
from esphome.config import read_config
from esphome.core import CORE
@pytest.fixture(scope="module")
def main_cpp(request: pytest.FixtureRequest) -> str:
"""Generate the C++ output for the shared widget-state YAML config once
per module.
Module-scoped so the (relatively expensive) codegen runs a single time;
the function-scoped fixtures from ``conftest.py`` (e.g. ``generate_main``)
can't be requested from a higher-scoped fixture, so the small amount of
setup is inlined here. The captured string is independent of
``CORE.reset()`` calls that the per-test autouse fixtures perform after
this fixture has produced its value.
"""
config_path = Path(request.fspath).parent / "config" / "widget_state_test.yaml"
original_path = CORE.config_path
try:
CORE.config_path = config_path
CORE.config = read_config({})
generate_cpp_contents(CORE.config)
return CORE.cpp_global_section + CORE.cpp_main_section
finally:
CORE.config_path = original_path
CORE.reset()
def test_static_state_emits_set_state_value(main_cpp: str) -> None:
"""A widget with ``state: { checked: true, disabled: false }`` should
generate one ``lv_obj_set_state_value`` call per entry, with the
appropriate boolean argument.
"""
assert (
"LvglComponent::lv_obj_set_state_value(btn_static, LV_STATE_CHECKED, true)"
in main_cpp
)
assert (
"LvglComponent::lv_obj_set_state_value(btn_static, LV_STATE_DISABLED, false)"
in main_cpp
)
def test_lambda_state_emits_set_state_value_with_lambda(main_cpp: str) -> None:
"""A widget with ``state: { pressed: !lambda return true; }`` should
generate ``lv_obj_set_state_value(..., LV_STATE_PRESSED, <expr>)`` where
``<expr>`` is the lambda's return value (cast or inlined), not a static
bool.
"""
# The set_state call is emitted for the templated state.
assert (
"LvglComponent::lv_obj_set_state_value(btn_lambda, LV_STATE_PRESSED,"
in main_cpp
)
# And it must NOT have collapsed the lambda to a literal true/false.
assert (
"LvglComponent::lv_obj_set_state_value(btn_lambda, LV_STATE_PRESSED, true)"
not in main_cpp
)
# The legacy if/else over add_state/remove_state is gone.
assert "lv_obj_add_state(btn_lambda, LV_STATE_PRESSED)" not in main_cpp
assert "lv_obj_remove_state(btn_lambda, LV_STATE_PRESSED)" not in main_cpp
def test_widget_disable_action_uses_set_state_value(main_cpp: str) -> None:
"""``lvgl.widget.disable: btn_actions`` should emit a
``set_state_value(..., LV_STATE_DISABLED, true)`` call rather than the
legacy ``lv_obj_add_state``.
"""
assert (
"LvglComponent::lv_obj_set_state_value(btn_actions, LV_STATE_DISABLED, true)"
in main_cpp
)
# No leftover legacy add_state for the DISABLED state of this widget.
assert "lv_obj_add_state(btn_actions, LV_STATE_DISABLED)" not in main_cpp
def test_widget_enable_action_uses_set_state_value(main_cpp: str) -> None:
"""``lvgl.widget.enable: btn_actions`` should emit a
``set_state_value(..., LV_STATE_DISABLED, false)`` call rather than the
legacy ``lv_obj_remove_state``.
"""
assert (
"LvglComponent::lv_obj_set_state_value(btn_actions, LV_STATE_DISABLED, false)"
in main_cpp
)
assert "lv_obj_remove_state(btn_actions, LV_STATE_DISABLED)" not in main_cpp
def test_buttonmatrix_disable_action_uses_helper(main_cpp: str) -> None:
"""``lvgl.widget.disable: matrix_btn_a`` should route through the new
``lv_buttonmatrix_set_button_ctrl_value`` helper for button index 0
with the ``DISABLED`` control bit set to ``true``, instead of the
legacy ``lv_buttonmatrix_set_button_ctrl``.
The button matrix obj is the compound's ``obj`` member and the index
is the position of the button in the row layout.
"""
assert (
"LvglComponent::lv_buttonmatrix_set_button_ctrl_value(matrix->obj, 0, "
"LV_BUTTONMATRIX_CTRL_DISABLED, true)"
) in main_cpp
def test_buttonmatrix_enable_action_uses_helper(main_cpp: str) -> None:
"""``lvgl.widget.enable: matrix_btn_a`` should route through the new
``lv_buttonmatrix_set_button_ctrl_value`` helper for button index 0
with the ``DISABLED`` control bit set to ``false``, instead of the
legacy ``lv_buttonmatrix_clear_button_ctrl``.
"""
assert (
"LvglComponent::lv_buttonmatrix_set_button_ctrl_value(matrix->obj, 0, "
"LV_BUTTONMATRIX_CTRL_DISABLED, false)"
) in main_cpp
# The legacy clear_button_ctrl path is gone for the matrix button enable
# action.
assert (
"lv_buttonmatrix_clear_button_ctrl(matrix->obj, 0, LV_BUTTONMATRIX_CTRL_DISABLED)"
not in main_cpp
)
def test_lvgl_switch_control_calls_set_state_value(main_cpp: str) -> None:
"""The LVGL switch platform installs a control lambda that mirrors the
switch's bool value into ``LV_STATE_CHECKED`` via
``lv_obj_set_state_value`` (replacing the previous if/else over
``add_state``/``clear_state`` plus an explicit ``send_event`` of
``lv_api_event``).
"""
# The control lambda calls the new helper with the bool ``v`` parameter.
assert (
"LvglComponent::lv_obj_set_state_value(switch_widget, LV_STATE_CHECKED, v)"
in main_cpp
)
# The deprecated lv_api_event symbol must no longer appear anywhere.
assert "lv_api_event" not in main_cpp
def test_default_state_does_not_emit_set_state_value(main_cpp: str) -> None:
"""A widget without a ``state:`` block must not generate any
``lv_obj_set_state_value`` calls for it. (Sanity-check that the
new code path is opt-in driven by the YAML.)
"""
assert (
"LvglComponent::lv_obj_set_state_value(switch_widget, LV_STATE_DISABLED"
not in main_cpp
)
assert (
"LvglComponent::lv_obj_set_state_value(btn_static, LV_STATE_PRESSED"
not in main_cpp
)