mirror of
https://github.com/esphome/esphome.git
synced 2026-10-05 10:31:29 +00:00
[lvgl] Dynamic rotation features (#16773)
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
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:
|
||||
# A flex container whose layout options are changed at runtime.
|
||||
- obj:
|
||||
id: flex_box
|
||||
layout:
|
||||
type: flex
|
||||
flex_flow: row
|
||||
widgets:
|
||||
- label:
|
||||
text: a
|
||||
- label:
|
||||
text: b
|
||||
|
||||
# A grid container whose alignment options are changed at runtime.
|
||||
# The grid structure (rows/columns) is fixed here at creation.
|
||||
- obj:
|
||||
id: grid_box
|
||||
layout:
|
||||
type: grid
|
||||
grid_rows: [content, content]
|
||||
grid_columns: [fr(1), fr(1)]
|
||||
widgets:
|
||||
- label:
|
||||
text: c
|
||||
- label:
|
||||
text: d
|
||||
|
||||
# Button hosting all of the update actions under test.
|
||||
- button:
|
||||
id: btn_actions
|
||||
on_click:
|
||||
# Update flex container options (type unchanged).
|
||||
- lvgl.widget.update:
|
||||
id: flex_box
|
||||
layout:
|
||||
flex_flow: column
|
||||
flex_align_main: center
|
||||
flex_align_cross: end
|
||||
pad_row: 7px
|
||||
# Update grid container alignment options (structure unchanged).
|
||||
- lvgl.widget.update:
|
||||
id: grid_box
|
||||
layout:
|
||||
grid_column_align: space_between
|
||||
grid_row_align: center
|
||||
# Top-level layout applies to the active screen.
|
||||
- lvgl.update:
|
||||
layout:
|
||||
flex_flow: column
|
||||
pad_column: 5px
|
||||
# Layout applied to the top display layer.
|
||||
- lvgl.update:
|
||||
top_layer:
|
||||
layout:
|
||||
flex_flow: row
|
||||
# Styling applied to the bottom display layer (exercises the
|
||||
# layers code path that previously generated no code).
|
||||
- lvgl.update:
|
||||
bottom_layer:
|
||||
bg_color: 0x123456
|
||||
@@ -0,0 +1,208 @@
|
||||
"""Tests for updating LVGL layout options via the update actions.
|
||||
|
||||
The ``lvgl.update`` and ``lvgl.widget.update`` (and per-widget
|
||||
``lvgl.<widget>.update``) actions can change a container's layout *options* at
|
||||
runtime. The layout ``type`` and the grid ``grid_rows``/``grid_columns``
|
||||
structure are fixed at widget creation (they determine the cells/options
|
||||
available to child widgets), so only the simple style options - those applied
|
||||
via ``lv_obj_set_style_...`` calls - may be changed.
|
||||
|
||||
These tests cover both the ``layout_validator`` (schema/normalisation) and the
|
||||
generated C++ for each target: a widget, the active screen (top-level
|
||||
``lvgl.update``) and the display layers.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from voluptuous import Invalid
|
||||
|
||||
from esphome.__main__ import generate_cpp_contents
|
||||
from esphome.components.lvgl.defines import TYPE_FLEX, TYPE_GRID, get_lv_uses
|
||||
from esphome.components.lvgl.layout import layout_validator
|
||||
from esphome.config import read_config
|
||||
from esphome.core import CORE
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# layout_validator - schema and normalisation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value,expected",
|
||||
[
|
||||
({"flex_flow": "row"}, {"flex_flow": "LV_FLEX_FLOW_ROW"}),
|
||||
({"flex_align_main": "center"}, {"flex_align_main": "LV_FLEX_ALIGN_CENTER"}),
|
||||
({"flex_align_cross": "end"}, {"flex_align_cross": "LV_FLEX_ALIGN_END"}),
|
||||
(
|
||||
{"grid_column_align": "space_between"},
|
||||
{"grid_column_align": "LV_GRID_ALIGN_SPACE_BETWEEN"},
|
||||
),
|
||||
({"grid_row_align": "center"}, {"grid_row_align": "LV_GRID_ALIGN_CENTER"}),
|
||||
({"pad_row": "7px"}, {"pad_row": 7}),
|
||||
({"pad_column": "5px"}, {"pad_column": 5}),
|
||||
],
|
||||
)
|
||||
def test_layout_validator_normalises_options(value: dict, expected: dict) -> None:
|
||||
"""Each supported option is accepted and normalised to its LVGL form."""
|
||||
assert layout_validator(value) == expected
|
||||
|
||||
|
||||
def test_layout_validator_accepts_multiple_options() -> None:
|
||||
"""Several options may be combined in one update."""
|
||||
result = layout_validator(
|
||||
{"flex_flow": "column", "flex_align_main": "center", "pad_row": "4px"}
|
||||
)
|
||||
assert result == {
|
||||
"flex_flow": "LV_FLEX_FLOW_COLUMN",
|
||||
"flex_align_main": "LV_FLEX_ALIGN_CENTER",
|
||||
"pad_row": 4,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
[
|
||||
{"type": "flex"},
|
||||
{"type": "grid", "grid_column_align": "center"},
|
||||
{"grid_rows": 3},
|
||||
{"grid_columns": ["fr(1)"]},
|
||||
{"grid_rows": [1, 2], "flex_flow": "row"},
|
||||
],
|
||||
)
|
||||
def test_layout_validator_rejects_structural_keys(value: dict) -> None:
|
||||
"""The layout type and grid structure are fixed at creation and must not
|
||||
be changeable via an update action."""
|
||||
with pytest.raises(Invalid, match="extra keys not allowed"):
|
||||
layout_validator(value)
|
||||
|
||||
|
||||
def test_layout_validator_rejects_empty() -> None:
|
||||
"""An update must specify at least one layout option."""
|
||||
with pytest.raises(Invalid, match="at least one layout option"):
|
||||
layout_validator({})
|
||||
|
||||
|
||||
def test_layout_validator_registers_flex_use() -> None:
|
||||
"""Validating a flex option registers the flex feature so LV_USE_FLEX is
|
||||
emitted even when the option is set solely via an update action."""
|
||||
layout_validator({"flex_flow": "row"})
|
||||
assert TYPE_FLEX in get_lv_uses()
|
||||
|
||||
|
||||
def test_layout_validator_registers_grid_use() -> None:
|
||||
"""Validating a grid option registers the grid feature."""
|
||||
layout_validator({"grid_column_align": "center"})
|
||||
assert TYPE_GRID in get_lv_uses()
|
||||
|
||||
|
||||
def test_pad_only_update_registers_no_layout_use() -> None:
|
||||
"""Padding options belong to both layout types, so they alone do not force
|
||||
either feature on."""
|
||||
layout_validator({"pad_row": "4px"})
|
||||
uses = get_lv_uses()
|
||||
assert TYPE_FLEX not in uses
|
||||
assert TYPE_GRID not in uses
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Generated C++ for the update actions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def main_cpp(request: pytest.FixtureRequest) -> str:
|
||||
"""Generate the C++ output for the shared layout-update YAML config once
|
||||
per module (codegen is relatively expensive)."""
|
||||
config_path = Path(request.fspath).parent / "config" / "layout_update_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_widget_flex_update_applies_partial_options(main_cpp: str) -> None:
|
||||
"""``lvgl.widget.update`` changes only the flex options that are specified,
|
||||
via the appropriate ``lv_obj_set_style_...``/``lv_obj_set_flex_flow``
|
||||
calls on the target widget."""
|
||||
assert "lv_obj_set_flex_flow(flex_box, LV_FLEX_FLOW_COLUMN)" in main_cpp
|
||||
assert (
|
||||
"lv_obj_set_style_flex_main_place(flex_box, LV_FLEX_ALIGN_CENTER, LV_STATE_DEFAULT)"
|
||||
in main_cpp
|
||||
)
|
||||
assert (
|
||||
"lv_obj_set_style_flex_cross_place(flex_box, LV_FLEX_ALIGN_END, LV_STATE_DEFAULT)"
|
||||
in main_cpp
|
||||
)
|
||||
assert "lv_obj_set_style_pad_row(flex_box, 7, LV_STATE_DEFAULT)" in main_cpp
|
||||
|
||||
|
||||
def test_widget_flex_update_does_not_change_type(main_cpp: str) -> None:
|
||||
"""The update must not re-establish the layout type: ``lv_obj_set_layout``
|
||||
is emitted once (at creation) and never from the update action."""
|
||||
assert main_cpp.count("lv_obj_set_layout(flex_box,") == 1
|
||||
|
||||
|
||||
def test_widget_flex_update_is_partial(main_cpp: str) -> None:
|
||||
"""An option that was not specified in the update (the track placement) is
|
||||
only set at creation, not by the partial update."""
|
||||
assert main_cpp.count("lv_obj_set_style_flex_track_place(flex_box,") == 1
|
||||
|
||||
|
||||
def test_widget_grid_update_applies_alignments(main_cpp: str) -> None:
|
||||
"""``lvgl.widget.update`` on a grid container changes its alignment
|
||||
options without touching the grid structure."""
|
||||
assert (
|
||||
"lv_obj_set_style_grid_column_align(grid_box, LV_GRID_ALIGN_SPACE_BETWEEN, "
|
||||
"LV_STATE_DEFAULT)" in main_cpp
|
||||
)
|
||||
assert (
|
||||
"lv_obj_set_style_grid_row_align(grid_box, LV_GRID_ALIGN_CENTER, LV_STATE_DEFAULT)"
|
||||
in main_cpp
|
||||
)
|
||||
|
||||
|
||||
def test_grid_update_does_not_regenerate_descriptor_arrays(main_cpp: str) -> None:
|
||||
"""The grid row/column descriptor arrays are structural and generated once
|
||||
at creation; an update must not regenerate them."""
|
||||
assert main_cpp.count("grid_box_row_dsc") != 0
|
||||
# The descriptor array is declared once and referenced once at creation.
|
||||
assert main_cpp.count("grid_box_row_dsc") == main_cpp.count("grid_box_column_dsc")
|
||||
assert "lv_obj_set_layout(grid_box," in main_cpp
|
||||
assert main_cpp.count("lv_obj_set_layout(grid_box,") == 1
|
||||
|
||||
|
||||
def test_top_level_layout_targets_active_screen(main_cpp: str) -> None:
|
||||
"""A top-level ``lvgl.update: { layout: ... }`` applies to the active
|
||||
screen, not to the LVGL component object."""
|
||||
assert (
|
||||
"lv_obj_set_flex_flow(lvgl_id->get_screen_active(), LV_FLEX_FLOW_COLUMN)"
|
||||
in main_cpp
|
||||
)
|
||||
assert (
|
||||
"lv_obj_set_style_pad_column(lvgl_id->get_screen_active(), 5, LV_STATE_DEFAULT)"
|
||||
in main_cpp
|
||||
)
|
||||
|
||||
|
||||
def test_top_layer_layout_applied(main_cpp: str) -> None:
|
||||
"""A layout under ``top_layer`` is applied to the display's top layer."""
|
||||
assert "lv_display_get_layer_top(lvgl_id->get_disp())" in main_cpp
|
||||
assert "lv_obj_set_flex_flow(top_layer_VAR_, LV_FLEX_FLOW_ROW)" in main_cpp
|
||||
|
||||
|
||||
def test_bottom_layer_styling_applied(main_cpp: str) -> None:
|
||||
"""A ``bottom_layer`` style update generates code (previously the layer
|
||||
keys of ``lvgl.update`` were silently ignored)."""
|
||||
assert "lv_display_get_layer_bottom(lvgl_id->get_disp())" in main_cpp
|
||||
assert (
|
||||
"lv_obj_set_style_bg_color(bottom_layer_VAR_, lv_color_make(18, 52, 86), "
|
||||
"LV_PART_MAIN)" in main_cpp
|
||||
)
|
||||
Reference in New Issue
Block a user