mirror of
https://github.com/esphome/esphome.git
synced 2026-09-30 08:20:21 +00:00
[lvgl] Animation updates and debug borders option (#19420)
This commit is contained in:
@@ -238,6 +238,7 @@ def multi_conf_validate(configs: list[dict]):
|
||||
CONF_COLOR_DEPTH,
|
||||
CONF_BYTE_ORDER,
|
||||
df.CONF_TRANSPARENCY_KEY,
|
||||
df.CONF_DEBUG_OUTLINE,
|
||||
):
|
||||
if base_config[item] != config[item]:
|
||||
raise cv.Invalid(
|
||||
@@ -393,6 +394,7 @@ async def to_code(configs):
|
||||
df.add_define("LV_FONT_DEFAULT", await lvalid.lv_font.process(default_font))
|
||||
cg.add(lvgl_static.esphome_lvgl_init())
|
||||
default_group = get_default_group(config_0)
|
||||
df.get_options()[df.CONF_DEBUG_OUTLINE] = config_0[df.CONF_DEBUG_OUTLINE]
|
||||
|
||||
for config in configs:
|
||||
frac = config[CONF_BUFFER_SIZE]
|
||||
@@ -634,6 +636,7 @@ LVGL_TOP_LEVEL_SCHEMA = (
|
||||
cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t),
|
||||
cv.Optional(df.CONF_RESUME_ON_INPUT, default=True): cv.boolean,
|
||||
cv.Optional(df.CONF_PAUSED, default=False): cv.boolean,
|
||||
cv.Optional(df.CONF_DEBUG_OUTLINE, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(DISP_BG_SCHEMA)
|
||||
|
||||
@@ -101,6 +101,31 @@ class LvAnimationTimingEaseInOut : public LvAnimationTiming {
|
||||
float slope_;
|
||||
};
|
||||
|
||||
class LvAnimationTimingEaseIn : public LvAnimationTiming {
|
||||
public:
|
||||
LvAnimationTimingEaseIn(float slope) : slope_(slope) {}
|
||||
float map_progress(float value) override {
|
||||
const float sqr = value * value;
|
||||
return this->slope_ * sqr + (1.0f - this->slope_) * value;
|
||||
}
|
||||
|
||||
protected:
|
||||
float slope_;
|
||||
};
|
||||
|
||||
class LvAnimationTimingEaseOut : public LvAnimationTiming {
|
||||
public:
|
||||
LvAnimationTimingEaseOut(float slope) : slope_(slope) {}
|
||||
float map_progress(float value) override {
|
||||
const float inv = 1.0f - value;
|
||||
const float sqr = 1.0f - inv * inv;
|
||||
return this->slope_ * sqr + (1.0f - this->slope_) * value;
|
||||
}
|
||||
|
||||
protected:
|
||||
float slope_;
|
||||
};
|
||||
|
||||
template<size_t DATA_SIZE, bool AUTO_START = false> class LvAnimation : public Component {
|
||||
public:
|
||||
LvAnimation(void (*update_callback)(const lv_coord_t *data), std::vector<TemplatableValue<lv_coord_t>> from,
|
||||
|
||||
@@ -76,6 +76,14 @@ TIMING_SCHEMA = cv.maybe_simple_value(
|
||||
"ease_in_out",
|
||||
{cv.Optional(CONF_WEIGHT, default=1.0): cv.zero_to_one_float},
|
||||
),
|
||||
timing_class(
|
||||
"ease_in",
|
||||
{cv.Optional(CONF_WEIGHT, default=1.0): cv.zero_to_one_float},
|
||||
),
|
||||
timing_class(
|
||||
"ease_out",
|
||||
{cv.Optional(CONF_WEIGHT, default=1.0): cv.zero_to_one_float},
|
||||
),
|
||||
timing_class(
|
||||
"gravity",
|
||||
{
|
||||
|
||||
@@ -35,6 +35,24 @@ KEY_WIDGET_MAP = "widget_map"
|
||||
KEY_WIDGETS_COMPLETED = "widgets_completed"
|
||||
KEY_OPTIONS = "options"
|
||||
KEY_WARNINGS = "warnings"
|
||||
KEY_DEBUG_OUTLINE_COUNT = "debug_outline_count"
|
||||
|
||||
# Colours for the debug outline, in (red, green, blue) order. They are picked to stay
|
||||
# distinct from each other and to show up on both light and dark backgrounds.
|
||||
DEBUG_OUTLINE_COLORS = (
|
||||
(255, 0, 0),
|
||||
(0, 160, 0),
|
||||
(0, 0, 255),
|
||||
(255, 140, 0),
|
||||
(200, 0, 200),
|
||||
(0, 190, 190),
|
||||
(160, 100, 0),
|
||||
(255, 0, 120),
|
||||
(110, 110, 110),
|
||||
(140, 200, 0),
|
||||
(0, 110, 255),
|
||||
(130, 0, 255),
|
||||
)
|
||||
|
||||
# Initial set of LVGL features that are always enabled.
|
||||
_INITIAL_LV_USES = frozenset(
|
||||
@@ -102,6 +120,15 @@ def get_options() -> dict[str, Any]:
|
||||
return _get_data(KEY_OPTIONS, {})
|
||||
|
||||
|
||||
def next_debug_outline_color() -> tuple[int, int, int]:
|
||||
"""Return the next debug outline colour, cycling through the palette."""
|
||||
# A one-element list so that the count can be updated in place.
|
||||
count = _get_data(KEY_DEBUG_OUTLINE_COUNT, [0])
|
||||
color = DEBUG_OUTLINE_COLORS[count[0] % len(DEBUG_OUTLINE_COLORS)]
|
||||
count[0] += 1
|
||||
return color
|
||||
|
||||
|
||||
def get_defines() -> dict[str, str]:
|
||||
return _get_data(KEY_LV_DEFINES, {})
|
||||
|
||||
@@ -684,6 +711,7 @@ CONF_COLOR_END = "color_end"
|
||||
CONF_COLOR_START = "color_start"
|
||||
CONF_CONTAINER = "container"
|
||||
CONF_CONTROL = "control"
|
||||
CONF_DEBUG_OUTLINE = "debug_outline"
|
||||
CONF_DEFAULT_FONT = "default_font"
|
||||
CONF_DEFAULT_GROUP = "default_group"
|
||||
CONF_DIR = "dir"
|
||||
|
||||
@@ -157,10 +157,12 @@ padding = LValidator(padding_validator, int32, retmapper=literal)
|
||||
|
||||
|
||||
def scale_validator(value):
|
||||
return cv.float_range(0.1, 10.0)(value)
|
||||
return cv.float_range(0.0, 10.0)(value)
|
||||
|
||||
|
||||
scale = LValidator(scale_validator, uint32, retmapper=lambda x: int(x * 256))
|
||||
scale = LValidator(
|
||||
scale_validator, uint32, retmapper=lambda x: int(x * 256), animatable=True
|
||||
)
|
||||
|
||||
|
||||
def angle(value):
|
||||
|
||||
@@ -994,6 +994,7 @@ lv_point_t LvglComponent::get_touch_relative_to_obj(lv_obj_t *obj) {
|
||||
|
||||
static void lv_container_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj) {
|
||||
LV_TRACE_OBJ_CREATE("begin");
|
||||
lv_obj_remove_flag(obj, LV_OBJ_FLAG_CLICKABLE);
|
||||
LV_UNUSED(class_p);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from esphome.schema_extractors import EnableSchemaExtraction
|
||||
from esphome.types import Expression
|
||||
|
||||
from ..defines import (
|
||||
CONF_DEBUG_OUTLINE,
|
||||
CONF_FLEX_ALIGN_CROSS,
|
||||
CONF_FLEX_ALIGN_MAIN,
|
||||
CONF_FLEX_ALIGN_TRACK,
|
||||
@@ -42,12 +43,14 @@ from ..defines import (
|
||||
STATES,
|
||||
LValidator,
|
||||
add_lv_use,
|
||||
get_options,
|
||||
get_styles_used,
|
||||
get_theme_widget_map,
|
||||
get_widget_map,
|
||||
get_widgets_completed,
|
||||
join_enums,
|
||||
literal,
|
||||
next_debug_outline_color,
|
||||
)
|
||||
from ..lv_validation import lv_int
|
||||
from ..lvcode import (
|
||||
@@ -191,6 +194,7 @@ class WidgetType:
|
||||
w = Widget.create(wid, var, self, config)
|
||||
apply_theme_styles(w)
|
||||
await set_obj_properties(w, config)
|
||||
apply_debug_outline(w)
|
||||
await add_widgets(w, config)
|
||||
await self.to_code(w, config)
|
||||
return w
|
||||
@@ -270,6 +274,21 @@ def apply_theme_styles(w: "Widget") -> None:
|
||||
w.add_style(style, lv_state)
|
||||
|
||||
|
||||
def apply_debug_outline(w: "Widget") -> None:
|
||||
"""
|
||||
When `debug_outline` is set, outline this widget in the next palette colour.
|
||||
An outline is drawn outside the widget's own box, so it doesn't take up layout
|
||||
space and doesn't touch the widget's own `border_*` style, unlike a border.
|
||||
"""
|
||||
if not get_options().get(CONF_DEBUG_OUTLINE):
|
||||
return
|
||||
r, g, b = next_debug_outline_color()
|
||||
w.set_style("outline_width", 1)
|
||||
w.set_style("outline_pad", 0)
|
||||
w.set_style("outline_color", f"lv_color_make({r}, {g}, {b})")
|
||||
w.set_style("outline_opa", "LV_OPA_COVER")
|
||||
|
||||
|
||||
class Widget:
|
||||
"""
|
||||
Represents a Widget.
|
||||
|
||||
@@ -57,6 +57,7 @@ from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t
|
||||
from . import (
|
||||
Widget,
|
||||
WidgetType,
|
||||
apply_debug_outline,
|
||||
apply_theme_styles,
|
||||
collect_parts,
|
||||
get_widgets,
|
||||
@@ -445,6 +446,7 @@ async def _finish_dynamic_widget(
|
||||
await w.type.on_create(w.obj, w_conf)
|
||||
apply_theme_styles(w)
|
||||
await set_obj_properties(w, w_conf)
|
||||
apply_debug_outline(w)
|
||||
await w.type.to_code(w, w_conf)
|
||||
await _wire_dynamic_triggers(w, w_conf)
|
||||
for child in w_conf.get(CONF_WIDGETS, ()):
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
esphome:
|
||||
name: test-debug-outline
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin:
|
||||
number: GPIO3
|
||||
|
||||
lvgl:
|
||||
debug_outline: true
|
||||
widgets:
|
||||
- obj:
|
||||
id: root_obj
|
||||
widgets:
|
||||
- label:
|
||||
id: title_label
|
||||
text: Title
|
||||
- button:
|
||||
id: ok_button
|
||||
widgets:
|
||||
- label:
|
||||
text: OK
|
||||
@@ -0,0 +1,35 @@
|
||||
esphome:
|
||||
name: test-debug-outline
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin:
|
||||
number: GPIO3
|
||||
|
||||
lvgl:
|
||||
widgets:
|
||||
- obj:
|
||||
id: root_obj
|
||||
widgets:
|
||||
- label:
|
||||
id: title_label
|
||||
text: Title
|
||||
- button:
|
||||
id: ok_button
|
||||
widgets:
|
||||
- label:
|
||||
text: OK
|
||||
@@ -191,6 +191,24 @@ class TestTimingSchema:
|
||||
result = TIMING_SCHEMA({"type": "ease_in_out", "weight": 0.5})
|
||||
assert result["weight"] == pytest.approx(0.5)
|
||||
|
||||
def test_ease_in_default_weight(self) -> None:
|
||||
result = TIMING_SCHEMA("ease_in")
|
||||
assert result["type"] == "ease_in"
|
||||
assert result["weight"] == pytest.approx(1.0)
|
||||
|
||||
def test_ease_in_custom_weight(self) -> None:
|
||||
result = TIMING_SCHEMA({"type": "ease_in", "weight": 0.5})
|
||||
assert result["weight"] == pytest.approx(0.5)
|
||||
|
||||
def test_ease_out_default_weight(self) -> None:
|
||||
result = TIMING_SCHEMA("ease_out")
|
||||
assert result["type"] == "ease_out"
|
||||
assert result["weight"] == pytest.approx(1.0)
|
||||
|
||||
def test_ease_out_custom_weight(self) -> None:
|
||||
result = TIMING_SCHEMA({"type": "ease_out", "weight": 0.5})
|
||||
assert result["weight"] == pytest.approx(0.5)
|
||||
|
||||
def test_gravity_defaults(self) -> None:
|
||||
result = TIMING_SCHEMA("gravity")
|
||||
assert result["type"] == "gravity"
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Tests for the LVGL ``debug_outline`` option code generation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
_OUTLINE_COLOR_RE = re.compile(
|
||||
r"lv_obj_set_style_outline_color\(.+?, (lv_color_make\(.+?\)),"
|
||||
)
|
||||
_OUTLINE_WIDTH_RE = re.compile(r"lv_obj_set_style_outline_width\(")
|
||||
_OUTLINE_PAD_RE = re.compile(r"lv_obj_set_style_outline_pad\(.+?, 0,")
|
||||
|
||||
|
||||
class TestDebugOutlineCodeGeneration:
|
||||
"""Verify that ``debug_outline`` outlines every widget in a distinct colour."""
|
||||
|
||||
def test_debug_outline_styles_every_widget(
|
||||
self, generate_main, component_config_path
|
||||
):
|
||||
"""Four widgets are declared, so four outlines with different colours appear."""
|
||||
main_cpp = generate_main(component_config_path("debug_outline.yaml"))
|
||||
assert len(_OUTLINE_WIDTH_RE.findall(main_cpp)) == 4
|
||||
assert len(_OUTLINE_PAD_RE.findall(main_cpp)) == 4
|
||||
colors = _OUTLINE_COLOR_RE.findall(main_cpp)
|
||||
assert len(colors) == 4
|
||||
assert len(set(colors)) == 4
|
||||
|
||||
def test_debug_outline_default_emits_nothing(
|
||||
self, generate_main, component_config_path
|
||||
):
|
||||
"""Without ``debug_outline`` no outline styles are generated."""
|
||||
main_cpp = generate_main(component_config_path("no_debug_outline.yaml"))
|
||||
assert "lv_obj_set_style_outline_" not in main_cpp
|
||||
@@ -9,13 +9,16 @@ from esphome.components.lvgl.schemas import theme_schema
|
||||
from esphome.config_validation import Invalid
|
||||
|
||||
|
||||
def _config(displays: list[str], theme: dict | None = None) -> dict:
|
||||
def _config(
|
||||
displays: list[str], theme: dict | None = None, debug_outline: bool = False
|
||||
) -> dict:
|
||||
config = {
|
||||
df.CONF_DISPLAYS: displays,
|
||||
"log_level": "WARN",
|
||||
"color_depth": 16,
|
||||
"byte_order": "big_endian",
|
||||
df.CONF_TRANSPARENCY_KEY: 0x000400,
|
||||
df.CONF_DEBUG_OUTLINE: debug_outline,
|
||||
}
|
||||
if theme is not None:
|
||||
config[df.CONF_THEME] = theme
|
||||
@@ -53,3 +56,20 @@ class TestThemeOnMultipleInstances:
|
||||
def test_passes_when_no_instance_has_theme(self) -> None:
|
||||
configs = [_config(["disp_a"]), _config(["disp_b"])]
|
||||
multi_conf_validate(configs)
|
||||
|
||||
|
||||
class TestDebugOutlineOnMultipleInstances:
|
||||
def test_raises_when_instances_differ(self) -> None:
|
||||
configs = [
|
||||
_config(["disp_a"], debug_outline=True),
|
||||
_config(["disp_b"], debug_outline=False),
|
||||
]
|
||||
with pytest.raises(Invalid, match="'debug_outline' must be the same"):
|
||||
multi_conf_validate(configs)
|
||||
|
||||
def test_passes_when_instances_match(self) -> None:
|
||||
configs = [
|
||||
_config(["disp_a"], debug_outline=True),
|
||||
_config(["disp_b"], debug_outline=True),
|
||||
]
|
||||
multi_conf_validate(configs)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Kept separate from the main lvgl-package.yaml so that enabling debug_outline
|
||||
# here doesn't add its extra style calls to the memory-impact baseline that
|
||||
# every other lvgl test change is measured against.
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
spi_id: spi_bus
|
||||
model: st7789v
|
||||
id: debug_outline_display
|
||||
dimensions:
|
||||
width: 240
|
||||
height: 320
|
||||
data_rate: 80MHz
|
||||
cs_pin: GPIO22
|
||||
dc_pin: GPIO21
|
||||
auto_clear_enabled: false
|
||||
update_interval: never
|
||||
|
||||
lvgl:
|
||||
debug_outline: true
|
||||
displays:
|
||||
- debug_outline_display
|
||||
widgets:
|
||||
- obj:
|
||||
id: debug_outline_root
|
||||
widgets:
|
||||
- label:
|
||||
text: Title
|
||||
- button:
|
||||
widgets:
|
||||
- label:
|
||||
text: OK
|
||||
@@ -40,6 +40,10 @@ lvgl:
|
||||
- round_trip
|
||||
- type: ease_in_out
|
||||
weight: 0.5
|
||||
- type: ease_in
|
||||
weight: 0.3
|
||||
- type: ease_out
|
||||
weight: 0.7
|
||||
on_start:
|
||||
- logger.log: anim started
|
||||
on_stop:
|
||||
|
||||
Reference in New Issue
Block a user