mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[lvgl] Add lvgl.widget.set_z_index action (#17993)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0a99b007a9
commit
97d33a5679
@@ -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
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user