[lvgl] Fixes #2 (#15161)

This commit is contained in:
Clyde Stubbs
2026-04-01 06:29:57 +10:00
committed by GitHub
parent 26b426bbff
commit da6c4e20fe
12 changed files with 145 additions and 72 deletions

View File

@@ -136,7 +136,7 @@ async def update_to_code(config, action_id, template_arg, args):
widget.type.w_type.value_property is not None
and widget.type.w_type.value_property in config
):
lv.event_send(widget.obj, UPDATE_EVENT, nullptr)
lv_obj.send_event(widget.obj, UPDATE_EVENT, nullptr)
widgets = await get_widgets(config[CONF_ID])
return await action_to_code(
@@ -455,6 +455,6 @@ async def obj_refresh_to_code(config, action_id, template_arg, args):
widget.type.w_type.value_property is not None
and widget.type.w_type.value_property in config
):
lv.event_send(widget.obj, UPDATE_EVENT, nullptr)
lv_obj.send_event(widget.obj, UPDATE_EVENT, nullptr)
return await action_to_code(widget, do_refresh, action_id, template_arg, args)

View File

@@ -541,6 +541,7 @@ CONF_END_ANGLE = "end_angle"
CONF_END_VALUE = "end_value"
CONF_ENTER_BUTTON = "enter_button"
CONF_ENTRIES = "entries"
CONF_EXT_CLICK_AREA = "ext_click_area"
CONF_FLAGS = "flags"
CONF_FLEX_FLOW = "flex_flow"
CONF_FLEX_ALIGN_MAIN = "flex_align_main"

View File

@@ -253,14 +253,10 @@ class MockLv:
A mock object that can be used to generate LVGL calls.
"""
# Mapping for LVGL 9
ATTR_MAP = {"event_send": "obj_send_event", "dither": "bg_dither_mode"}
def __init__(self, base):
self.base = base
def __getattr__(self, attr: str) -> "MockLv":
attr = MockLv.ATTR_MAP.get(attr, attr)
return MockLv(f"{self.base}{attr}")
def append(self, expression):
@@ -314,7 +310,6 @@ class ReturnStatement(ExpressionStatement):
class LvExpr(MockLv):
def __getattr__(self, attr: str) -> "MockLv":
attr = MockLv.ATTR_MAP.get(attr, attr)
return LvExpr(f"{self.base}{attr}")
def append(self, expression):

View File

@@ -343,26 +343,26 @@ void IndicatorLine::set_value(int value) {
}
void IndicatorLine::update_length_() {
uint32_t actual_needle_length;
auto radius = lv_obj_get_width(lv_obj_get_parent(this->obj)) / 2;
auto cx = lv_obj_get_width(lv_obj_get_parent(this->obj)) / 2;
auto cy = lv_obj_get_height(lv_obj_get_parent(this->obj)) / 2;
auto radius = clamp_at_most(cx, cy);
auto length = lv_obj_get_style_length(this->obj, LV_PART_MAIN);
auto radial_offset = lv_obj_get_style_radial_offset(this->obj, LV_PART_MAIN);
if (LV_COORD_IS_PCT(radial_offset)) {
radial_offset = radius * LV_COORD_GET_PCT(radial_offset) / 100;
}
if (LV_COORD_IS_PCT(length)) {
actual_needle_length = radius * LV_COORD_GET_PCT(length) / 100;
length = radius * LV_COORD_GET_PCT(length) / 100;
} else if (length < 0) {
actual_needle_length = radius + length;
} else {
actual_needle_length = length;
length += radius;
}
auto x = lv_trigo_cos(this->angle_) / 32768.0f;
auto y = lv_trigo_sin(this->angle_) / 32768.0f;
// radius here also represents the offset of the scale center from top left
this->points_[0].x = radius + radial_offset * x;
this->points_[0].y = radius + radial_offset * y;
this->points_[1].x = x * actual_needle_length + radius;
this->points_[1].y = y * actual_needle_length + radius;
this->points_[1].x = radius + x * (radial_offset + length);
this->points_[1].y = radius + y * (radial_offset + length);
lv_obj_refresh_self_size(this->obj);
lv_obj_invalidate(this->obj);
}
@@ -682,15 +682,15 @@ void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_en
auto *line_dsc = static_cast<lv_draw_line_dsc_t *>(lv_draw_task_get_draw_dsc(task));
int tick = line_dsc->base.id2;
if (tick >= range_start && tick <= range_end) {
unsigned range = range_end - range_start;
int ratio;
if (local) {
int range = range_end - range_start;
tick -= range_start;
ratio = range == 0 ? 0 : (tick * 255) / range;
} else {
range = lv_scale_get_total_tick_count(scale) - 1;
// total tick count is guaranteed to be at least 2.
ratio = (line_dsc->base.id1 * 255) / (lv_scale_get_total_tick_count(scale) - 1);
}
if (range == 0)
range = 1;
auto ratio = (tick * 255) / range;
line_dsc->color = lv_color_mix(color_end, color_start, ratio);
line_dsc->width += width;
}

View File

@@ -12,7 +12,7 @@ from ..lvcode import (
UPDATE_EVENT,
LambdaContext,
ReturnStatement,
lv,
lv_obj,
lvgl_static,
)
from ..types import LV_EVENT, LvNumber, lvgl_ns
@@ -40,7 +40,7 @@ async def to_code(config):
await widget.set_property(
"value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED]
)
lv.event_send(widget.obj, API_EVENT, cg.nullptr)
lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr)
event_code = (
LV_EVENT.VALUE_CHANGED
if not config[CONF_UPDATE_ON_RELEASE]

View File

@@ -146,26 +146,41 @@ def point_schema(value):
# All LVGL styles and their validators
STYLE_PROPS = {
BASE_PROPS = {
"align": df.CHILD_ALIGNMENTS.one_of,
"arc_opa": lvalid.opacity,
"anim_duration": lvalid.lv_milliseconds,
"arc_color": lvalid.lv_color,
"arc_opa": lvalid.opacity,
"arc_rounded": lvalid.lv_bool,
"arc_width": lvalid.pixels,
"anim_time": lvalid.lv_milliseconds,
"base_dir": df.LvConstant("LV_BASE_DIR_", "LTR", "RTL", "AUTO").one_of,
"bg_color": lvalid.lv_color,
"bg_grad": lv_gradient,
"bg_grad_color": lvalid.lv_color,
"bg_dither_mode": df.LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF").one_of,
"bg_grad_dir": LV_GRAD_DIR.one_of,
"bg_grad_opa": lvalid.opacity,
"bg_grad_stop": lvalid.stop_value,
"bg_image_opa": lvalid.opacity,
"bg_image_recolor": lvalid.lv_color,
"bg_image_recolor_opa": lvalid.opacity,
"bg_image_src": lvalid.lv_image,
"bg_image_tiled": lvalid.lv_bool,
"bg_main_opa": lvalid.opacity,
"bg_main_stop": lvalid.stop_value,
"bg_opa": lvalid.opacity,
"blend_mode": df.LvConstant(
"LV_BLEND_MODE_",
"NORMAL",
"ADDITIVE",
"SUBTRACTIVE",
"MULTIPLY",
"DIFFERENCE",
).one_of,
"blur_backdrop": lvalid.lv_bool,
"blur_quality": df.LvConstant(
"LV_BLUR_QUALITY_", "AUTO", "SPEED", "PRECISION"
).one_of,
"blur_radius": lvalid.lv_positive_int,
"border_color": lvalid.lv_color,
"border_opa": lvalid.opacity,
"border_post": lvalid.lv_bool,
@@ -175,33 +190,53 @@ STYLE_PROPS = {
"border_width": lvalid.lv_positive_int,
"clip_corner": lvalid.lv_bool,
"color_filter_opa": lvalid.opacity,
"drop_shadow_color": lvalid.lv_color,
"drop_shadow_offset_x": lvalid.lv_int,
"drop_shadow_offset_y": lvalid.lv_int,
"drop_shadow_opa": lvalid.opacity,
"drop_shadow_quality": df.LvConstant(
"LV_BLUR_QUALITY_", "AUTO", "SPEED", "PRECISION"
).one_of,
"drop_shadow_radius": lvalid.lv_positive_int,
"height": lvalid.size,
"image_opa": lvalid.opacity,
"image_recolor": lvalid.lv_color,
"image_recolor_opa": lvalid.opacity,
"length": lvalid.pixels_or_percent,
"line_color": lvalid.lv_color,
"line_dash_gap": lvalid.lv_positive_int,
"line_dash_width": lvalid.lv_positive_int,
"line_opa": lvalid.opacity,
"line_rounded": lvalid.lv_bool,
"line_width": lvalid.lv_positive_int,
"margin_bottom": lvalid.padding,
"margin_left": lvalid.padding,
"margin_right": lvalid.padding,
"margin_top": lvalid.padding,
"max_height": lvalid.pixels_or_percent,
"max_width": lvalid.pixels_or_percent,
"min_height": lvalid.pixels_or_percent,
"min_width": lvalid.pixels_or_percent,
"opa": lvalid.opacity,
"opa_layered": lvalid.opacity,
"outline_color": lvalid.lv_color,
"outline_opa": lvalid.opacity,
"outline_pad": lvalid.padding,
"outline_width": lvalid.pixels,
"length": lvalid.pixels_or_percent,
"pad_all": lvalid.padding,
"pad_bottom": lvalid.padding,
"pad_left": lvalid.padding,
"pad_radial": lvalid.padding,
"pad_right": lvalid.padding,
"pad_top": lvalid.padding,
"radial_offset": lvalid.size,
"radius": lvalid.lv_fraction,
"recolor": lvalid.lv_color,
"recolor_opa": lvalid.opacity,
"rotary_sensitivity": lvalid.lv_positive_int,
"shadow_color": lvalid.lv_color,
"shadow_offset_x": lvalid.lv_int,
"shadow_offset_y": lvalid.lv_int,
"shadow_ofs_x": lvalid.lv_int,
"shadow_ofs_y": lvalid.lv_int,
"shadow_opa": lvalid.opacity,
"shadow_spread": lvalid.lv_int,
"shadow_width": lvalid.lv_positive_int,
@@ -216,7 +251,9 @@ STYLE_PROPS = {
"text_letter_space": lvalid.lv_positive_int,
"text_line_space": lvalid.lv_positive_int,
"text_opa": lvalid.opacity,
"transform_angle": lvalid.lv_angle,
"text_outline_stroke_color": lvalid.lv_color,
"text_outline_stroke_opa": lvalid.opacity,
"text_outline_stroke_width": lvalid.lv_positive_int,
"transform_height": lvalid.pixels_or_percent,
"transform_pivot_x": lvalid.pixels_or_percent,
"transform_pivot_y": lvalid.pixels_or_percent,
@@ -226,20 +263,17 @@ STYLE_PROPS = {
"transform_scale_y": lvalid.scale,
"transform_skew_x": lvalid.lv_angle,
"transform_skew_y": lvalid.lv_angle,
"transform_zoom": lvalid.scale,
"transform_width": lvalid.pixels_or_percent,
"translate_radial": lvalid.lv_int,
"translate_x": lvalid.pixels_or_percent,
"translate_y": lvalid.pixels_or_percent,
"max_height": lvalid.pixels_or_percent,
"max_width": lvalid.pixels_or_percent,
"min_height": lvalid.pixels_or_percent,
"min_width": lvalid.pixels_or_percent,
"radius": lvalid.lv_fraction,
"width": lvalid.size,
"x": lvalid.pixels_or_percent,
"y": lvalid.pixels_or_percent,
}
STYLE_REMAP = {
"anim_time": "anim_duration",
"transform_angle": "transform_rotation",
"transform_zoom": "transform_scale",
"zoom": "scale",
@@ -249,6 +283,10 @@ STYLE_REMAP = {
"r_mod": "length",
}
STYLE_PROPS = BASE_PROPS | {
p: BASE_PROPS[v] for p, v in STYLE_REMAP.items() if v in BASE_PROPS
}
def remap_property(prop, record=True):
"""
@@ -394,6 +432,7 @@ def obj_schema(widget_type: WidgetType):
return (
part_schema(widget_type.parts)
.extend(ALIGN_TO_SCHEMA)
.extend({cv.Optional(df.CONF_EXT_CLICK_AREA): lvalid.pixels})
.extend(automation_schema(widget_type.w_type))
.extend(
{

View File

@@ -13,8 +13,8 @@ from ..lvcode import (
LambdaContext,
LvConditional,
LvContext,
lv,
lv_add,
lv_obj,
lvgl_static,
)
from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns
@@ -39,7 +39,7 @@ async def to_code(config):
widget.add_state(LV_STATE.CHECKED)
cond.else_()
widget.clear_state(LV_STATE.CHECKED)
lv.event_send(widget.obj, API_EVENT, cg.nullptr)
lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr)
control.add(switch_id.publish_state(v))
switch = cg.new_Pvariable(config[CONF_ID], await control.get_lambda())
await cg.register_component(switch, config)

View File

@@ -10,8 +10,8 @@ from ..lvcode import (
UPDATE_EVENT,
LambdaContext,
LvContext,
lv,
lv_add,
lv_obj,
lvgl_static,
)
from ..types import LV_EVENT, LvText, lvgl_ns
@@ -33,7 +33,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.event_send(widget.obj, API_EVENT, cg.nullptr)
lv_obj.send_event(widget.obj, API_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()))

View File

@@ -15,6 +15,7 @@ from .defines import (
CONF_ALIGN,
CONF_ALIGN_TO,
CONF_ALIGN_TO_LAMBDA_ID,
CONF_EXT_CLICK_AREA,
DIRECTIONS,
LV_EVENT_MAP,
LV_EVENT_TRIGGERS,
@@ -113,6 +114,8 @@ async def generate_align_tos(config: dict):
x = align_to[CONF_X]
y = align_to[CONF_Y]
lv.obj_align_to(w.obj, target, align, x, y)
if ext_click_area := w.config.get(CONF_EXT_CLICK_AREA):
lv.obj_set_ext_click_area(w.obj, ext_click_area)
action_id = config[CONF_ALIGN_TO_LAMBDA_ID]
var = new_Pvariable(action_id, await context.get_lambda())

View File

@@ -56,11 +56,11 @@ from ..lv_validation import (
lv_float,
lv_image,
lv_int,
lv_positive_int,
opacity,
padding,
pixels,
pixels_or_percent,
pixels_or_percent_validator,
requires_component,
size,
)
@@ -88,7 +88,10 @@ CONF_COLOR_START = "color_start"
CONF_DRAW_TICKS_ON_TOP = "draw_ticks_on_top"
CONF_IMAGE_ID = "image_id"
CONF_INDICATORS = "indicators"
CONF_DASH_GAP = "dash_gap"
CONF_DASH_WIDTH = "dash_width"
CONF_LINE_ID = "line_id"
CONF_ROUNDED = "rounded"
CONF_LABEL_GAP = "label_gap"
CONF_MAJOR = "major"
CONF_METER = "meter"
@@ -135,9 +138,12 @@ INDICATOR_LINE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_WIDTH, default=4): cv.int_,
cv.Optional(CONF_COLOR, default=0): lv_color,
cv.Optional(CONF_ROUNDED, default=True): lv_bool,
cv.Optional(CONF_DASH_GAP): lv_positive_int,
cv.Optional(CONF_DASH_WIDTH): lv_positive_int,
cv.Optional(CONF_R_MOD): padding,
cv.Optional(CONF_LENGTH): pixels_or_percent_validator,
cv.Optional(CONF_RADIAL_OFFSET, 0): pixels_or_percent_validator,
cv.Optional(CONF_LENGTH): pixels_or_percent,
cv.Optional(CONF_RADIAL_OFFSET): pixels_or_percent,
cv.Optional(CONF_VALUE, default=0.0): lv_float,
cv.Optional(CONF_OPA, default=1.0): opacity,
}
@@ -249,17 +255,17 @@ SCALE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_COUNT, default=12): cv.int_range(min=2),
cv.Optional(CONF_WIDTH, default=2): cv.positive_int,
cv.Optional(CONF_LENGTH, default=10): size,
cv.Optional(CONF_RADIAL_OFFSET, default=0): size,
cv.Optional(CONF_LENGTH, default=10): cv.positive_int,
cv.Optional(CONF_RADIAL_OFFSET): cv.positive_int,
cv.Optional(CONF_COLOR, default=0x808080): lv_color,
cv.Optional(CONF_MAJOR): cv.Schema(
{
cv.Optional(CONF_STRIDE, default=3): cv.positive_int,
cv.Optional(CONF_WIDTH, default=5): size,
cv.Optional(CONF_LENGTH, default="15%"): size,
cv.Optional(CONF_RADIAL_OFFSET, default=0): size,
cv.Optional(CONF_LENGTH, default=12): cv.positive_int,
cv.Optional(CONF_RADIAL_OFFSET): cv.positive_int,
cv.Optional(CONF_COLOR, default=0): lv_color,
cv.Optional(CONF_LABEL_GAP, default=4): size,
cv.Optional(CONF_LABEL_GAP, default=4): cv.int_,
}
),
}
@@ -466,11 +472,15 @@ class MeterType(WidgetType):
CONF_OPA: v[CONF_OPA],
CONF_LINE_WIDTH: v[CONF_WIDTH],
"line_color": v[CONF_COLOR],
"line_rounded": True,
"line_rounded": v[CONF_ROUNDED],
CONF_ALIGN: CHILD_ALIGNMENTS.TOP_LEFT,
CONF_LENGTH: length,
CONF_RADIAL_OFFSET: v[CONF_RADIAL_OFFSET],
}
if radial_offset := v.get(CONF_RADIAL_OFFSET):
props[CONF_RADIAL_OFFSET] = radial_offset
for option in (CONF_DASH_WIDTH, CONF_DASH_GAP):
if option in v:
props["line_" + option] = v[option]
lw = await widget_to_code(props, line_indicator_type, scale_var)
await set_indicator_values(lw, v)
@@ -478,10 +488,8 @@ class MeterType(WidgetType):
add_lv_use(CONF_IMAGE)
src = v[CONF_SRC]
src_data = get_image_metadata(src.id)
pivot_x = await pixels.process(v[CONF_PIVOT_X])
pivot_y = await pixels.process(
v.get(CONF_PIVOT_Y, src_data.height // 2)
)
pivot_x = v[CONF_PIVOT_X]
pivot_y = v.get(CONF_PIVOT_Y, src_data.height // 2)
props = {
CONF_X: src_data.width // 2 - pivot_x,
"transform_pivot_x": pivot_x,
@@ -511,11 +519,12 @@ class MeterType(WidgetType):
lv_obj.set_style_line_width(
scale_var, await size.process(ticks[CONF_WIDTH]), LV_PART.ITEMS
)
lv_obj.set_style_radial_offset(
scale_var,
await size.process(ticks[CONF_RADIAL_OFFSET]),
LV_PART.ITEMS,
)
if radial_offset := ticks.get(CONF_RADIAL_OFFSET):
lv_obj.set_style_radial_offset(
scale_var,
-radial_offset,
LV_PART.ITEMS,
)
lv_obj.set_style_line_color(
scale_var,
await lv_color.process(ticks[CONF_COLOR]),
@@ -536,11 +545,12 @@ class MeterType(WidgetType):
await size.process(major[CONF_LENGTH]),
LV_PART.INDICATOR,
)
lv_obj.set_style_radial_offset(
scale_var,
await size.process(ticks[CONF_RADIAL_OFFSET]),
LV_PART.INDICATOR,
)
if radial_offset := major.get(CONF_RADIAL_OFFSET):
lv_obj.set_style_radial_offset(
scale_var,
-radial_offset,
LV_PART.INDICATOR,
)
lv_obj.set_style_line_width(
scale_var,
await size.process(major[CONF_WIDTH]),
@@ -553,12 +563,9 @@ class MeterType(WidgetType):
)
# Set label gap (padding)
label_gap = await size.process(major[CONF_LABEL_GAP])
if isinstance(label_gap, int):
label_gap -= DEFAULT_LABEL_GAP
lv_obj.set_style_pad_radial(
scale_var,
label_gap,
major[CONF_LABEL_GAP] - DEFAULT_LABEL_GAP,
LV_PART.INDICATOR,
)
else:

View File

@@ -129,6 +129,6 @@ async def tileview_select(config, action_id, template_arg, args):
lv.tileview_set_tile_by_index(
widgets[0].obj, column, row, literal(config[CONF_ANIMATED])
)
lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr)
lv_obj.send_event(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr)
return await action_to_code(widgets, do_select, action_id, template_arg, args)

View File

@@ -232,7 +232,7 @@ lvgl:
- roller:
id: lv_roller
visible_row_count: 2
anim_time: 500ms
anim_duration: 500ms
options:
- Nov
- Dec
@@ -317,20 +317,27 @@ lvgl:
align: top_left
- container:
align: center
anim_duration: 1s
arc_opa: COVER
arc_color: 0xFF0000
arc_rounded: false
arc_width: 3
anim_time: 1s
base_dir: auto
bg_color: light_blue
bg_grad_color: light_blue
bg_grad_dir: hor
bg_grad_opa: cover
bg_grad_stop: 128
bg_image_opa: transp
bg_image_recolor: light_blue
bg_image_recolor_opa: 50%
bg_main_opa: cover
bg_main_stop: 0
bg_opa: 20%
blend_mode: normal
blur_backdrop: false
blur_quality: auto
blur_radius: 0
border_color: 0x00FF00
border_opa: cover
border_post: true
@@ -338,7 +345,15 @@ lvgl:
border_width: 4
clip_corner: false
color_filter_opa: transp
drop_shadow_color: 0x000000
drop_shadow_offset_x: 5
drop_shadow_offset_y: 5
drop_shadow_opa: cover
drop_shadow_quality: precision
drop_shadow_radius: 10
ext_click_area: 100px
height: 50%
image_opa: cover
image_recolor: light_blue
image_recolor_opa: cover
line_width: 10
@@ -346,6 +361,10 @@ lvgl:
line_dash_gap: 10
line_rounded: false
line_color: light_blue
margin_bottom: 4
margin_left: 4
margin_right: 4
margin_top: 4
opa: cover
opa_layered: cover
outline_color: light_blue
@@ -355,8 +374,12 @@ lvgl:
pad_all: 10px
pad_bottom: 10px
pad_left: 10px
pad_radial: 0
pad_right: 10px
pad_top: 10px
recolor: 0xFF0000
recolor_opa: transp
rotary_sensitivity: 256
shadow_color: light_blue
shadow_opa: cover
shadow_spread: 5
@@ -368,6 +391,9 @@ lvgl:
text_letter_space: 4
text_line_space: 4
text_opa: cover
text_outline_stroke_color: 0x000000
text_outline_stroke_opa: cover
text_outline_stroke_width: 2
transform_rotation: 90
transform_height: 100
transform_pivot_x: 50%
@@ -377,8 +403,10 @@ lvgl:
transform_scale_y: 0.8
transform_skew_x: 10
transform_skew_y: 20
transform_width: 100
shadow_offset_x: 3
shadow_offset_y: 3
translate_radial: 0
translate_x: 10
translate_y: 10
max_height: 100
@@ -1053,7 +1081,7 @@ lvgl:
- ticks:
width: 1
count: 61
length: 20%
length: 20
radial_offset: 5
color: 0xFFFFFF
major: