[lvgl] Add lvgl.widget.set_z_index action (#17993)

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Clyde Stubbs
2026-08-05 14:14:32 +12:00
committed by GitHub
co-authored by Claude Sonnet 5
parent 0a99b007a9
commit 97d33a5679
4 changed files with 259 additions and 2 deletions
+52 -1
View File
@@ -5,7 +5,14 @@ from esphome import automation
from esphome.automation import StatelessLambdaAction
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_ROTATION, CONF_TIMEOUT
from esphome.const import (
CONF_ACTION,
CONF_GROUP,
CONF_ID,
CONF_POSITION,
CONF_ROTATION,
CONF_TIMEOUT,
)
from esphome.core import Lambda
from esphome.cpp_generator import TemplateArguments, get_variable
from esphome.cpp_types import nullptr
@@ -28,6 +35,7 @@ from .defines import (
get_focused_widgets,
get_options,
get_refreshed_widgets,
literal,
)
from .layout import layout_validator
from .lv_validation import lv_bool, lv_milliseconds, lv_rotation
@@ -36,6 +44,7 @@ from .lvcode import (
UPDATE_EVENT,
LambdaContext,
LocalVariable,
LvConditional,
LvglComponent,
ReturnStatement,
add_line_marks,
@@ -376,6 +385,48 @@ async def obj_show_to_code(config, action_id, template_arg, args):
return await action_to_code(widgets, do_show, action_id, template_arg, args)
SET_Z_INDEX_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.ensure_list(
cv.maybe_simple_value(
{cv.Required(CONF_ID): cv.use_id(lv_obj_t)},
key=CONF_ID,
)
),
cv.Required(CONF_POSITION): cv.Any(
cv.one_of("TOP", "BOTTOM", "UP", "DOWN", upper=True), cv.int_
),
}
)
@automation.register_action(
"lvgl.widget.set_z_index", ObjUpdateAction, SET_Z_INDEX_SCHEMA, synchronous=True
)
async def obj_set_z_index_to_code(config, action_id, template_arg, args):
position = config[CONF_POSITION]
async def do_set_z_index(widget: Widget):
if position == "TOP":
lv_obj.move_foreground(widget.obj)
elif position == "BOTTOM":
lv_obj.move_background(widget.obj)
elif position == "UP":
lv_obj.move_to_index(
widget.obj, literal(f"{lv_expr.obj_get_index(widget.obj)} + 1")
)
elif position == "DOWN":
with LvConditional(f"{lv_expr.obj_get_index(widget.obj)} > 0"):
lv_obj.move_to_index(
widget.obj, literal(f"{lv_expr.obj_get_index(widget.obj)} - 1")
)
else:
lv_obj.move_to_index(widget.obj, position)
widgets = [widget.outer or widget for widget in await get_widgets(config[CONF_ID])]
return await action_to_code(widgets, do_set_z_index, action_id, template_arg, args)
def focused_id(value):
value = cv.use_id(lv_pseudo_button_t)(value)
get_focused_widgets().add(value)
@@ -0,0 +1,60 @@
esphome:
name: test-set-z-index
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:
displays: tft_display
widgets:
- label:
id: label_a
text: "A"
- label:
id: label_b
text: "B"
- button:
id: trigger_btn
on_click:
- lvgl.widget.set_z_index:
id: label_a
position: top
- lvgl.widget.set_z_index:
id: label_a
position: bottom
- lvgl.widget.set_z_index:
id: label_a
position: up
- lvgl.widget.set_z_index:
id: label_a
position: down
- lvgl.widget.set_z_index:
id: label_a
position: 3
- lvgl.widget.set_z_index:
id: label_a
position: -2
- lvgl.widget.set_z_index:
id: [label_a, label_b]
position: up
@@ -0,0 +1,128 @@
"""Tests for the ``lvgl.widget.set_z_index`` action: schema validation and
code generation.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from esphome.components.lvgl.automation import SET_Z_INDEX_SCHEMA
from esphome.config_validation import Invalid
# ---------------------------------------------------------------------------
# Validation
# ---------------------------------------------------------------------------
class TestSetZIndexSchemaValidation:
"""Test that SET_Z_INDEX_SCHEMA accepts the documented forms and rejects
everything else.
"""
@pytest.mark.parametrize("position", ["top", "bottom", "up", "down"])
def test_keyword_position_accepted(self, position: str) -> None:
config = SET_Z_INDEX_SCHEMA({"id": "my_widget", "position": position})
assert config["position"] == position.upper()
@pytest.mark.parametrize("position", ["Top", "BOTTOM", "Up", "dOwN"])
def test_keyword_position_case_insensitive(self, position: str) -> None:
config = SET_Z_INDEX_SCHEMA({"id": "my_widget", "position": position})
assert config["position"] == position.upper()
@pytest.mark.parametrize("position", [0, 1, 5, -1, -5])
def test_integer_position_accepted(self, position: int) -> None:
config = SET_Z_INDEX_SCHEMA({"id": "my_widget", "position": position})
assert config["position"] == position
def test_unknown_keyword_rejected(self) -> None:
with pytest.raises(Invalid):
SET_Z_INDEX_SCHEMA({"id": "my_widget", "position": "sideways"})
def test_float_position_rejected(self) -> None:
with pytest.raises(Invalid):
SET_Z_INDEX_SCHEMA({"id": "my_widget", "position": 1.5})
def test_missing_id_rejected(self) -> None:
with pytest.raises(Invalid):
SET_Z_INDEX_SCHEMA({"position": "top"})
def test_missing_position_rejected(self) -> None:
with pytest.raises(Invalid):
SET_Z_INDEX_SCHEMA({"id": "my_widget"})
def test_single_id_is_wrapped_in_list(self) -> None:
config = SET_Z_INDEX_SCHEMA({"id": "my_widget", "position": "top"})
assert len(config["id"]) == 1
assert config["id"][0]["id"].id == "my_widget"
def test_list_of_ids_accepted(self) -> None:
config = SET_Z_INDEX_SCHEMA({"id": ["widget_a", "widget_b"], "position": "top"})
assert [entry["id"].id for entry in config["id"]] == ["widget_a", "widget_b"]
# ---------------------------------------------------------------------------
# Code generation
# ---------------------------------------------------------------------------
@pytest.fixture(scope="module")
def main_cpp(request: pytest.FixtureRequest) -> str:
"""Generate the C++ output for the shared set_z_index YAML config once
per module. See ``test_widget_state.py`` for why this is module-scoped
and self-contained rather than using the function-scoped ``generate_main``
fixture from ``conftest.py``.
"""
from esphome.__main__ import generate_cpp_contents
from esphome.config import read_config
from esphome.core import CORE
config_path = Path(request.fspath).parent / "config" / "set_z_index_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_top_emits_move_foreground(main_cpp: str) -> None:
assert "lv_obj_move_foreground(label_a);" in main_cpp
def test_bottom_emits_move_background(main_cpp: str) -> None:
assert "lv_obj_move_background(label_a);" in main_cpp
def test_up_emits_unguarded_index_increment(main_cpp: str) -> None:
assert "lv_obj_move_to_index(label_a, lv_obj_get_index(label_a) + 1);" in main_cpp
def test_down_emits_guarded_index_decrement(main_cpp: str) -> None:
"""``down`` must be guarded so that a widget already at index 0 isn't
reinterpreted by LVGL as "move to the top" (LVGL treats a negative
index as "count from the back").
"""
assert "if (lv_obj_get_index(label_a) > 0) {" in main_cpp
assert "lv_obj_move_to_index(label_a, lv_obj_get_index(label_a) - 1);" in main_cpp
def test_positive_integer_emits_direct_index(main_cpp: str) -> None:
assert "lv_obj_move_to_index(label_a, 3);" in main_cpp
def test_negative_integer_emits_direct_index(main_cpp: str) -> None:
assert "lv_obj_move_to_index(label_a, -2);" in main_cpp
def test_list_of_ids_applies_to_each_widget(main_cpp: str) -> None:
"""``id: [label_a, label_b]`` must emit the move call once per widget."""
assert (
main_cpp.count("lv_obj_move_to_index(label_a, lv_obj_get_index(label_a) + 1);")
== 2
)
assert "lv_obj_move_to_index(label_b, lv_obj_get_index(label_b) + 1);" in main_cpp
+19 -1
View File
@@ -756,7 +756,25 @@ lvgl:
on_defocus:
lvgl.widget.hide: hello_label
on_focus:
logger.log: Button clicked
- logger.log: Button clicked
- lvgl.widget.set_z_index:
id: hello_label
position: top
- lvgl.widget.set_z_index:
id: hello_label
position: bottom
- lvgl.widget.set_z_index:
id: hello_label
position: up
- lvgl.widget.set_z_index:
id: hello_label
position: down
- lvgl.widget.set_z_index:
id: hello_label
position: 1
- lvgl.widget.set_z_index:
id: hello_label
position: -1
on_scroll:
logger.log: Button clicked
on_scroll_end: