[lvgl] Add radial and conical gradients (#18818)

This commit is contained in:
Clyde Stubbs
2026-08-30 21:21:07 +10:00
committed by GitHub
parent a811aa840c
commit b9eda644cd
3 changed files with 239 additions and 24 deletions
+2 -1
View File
@@ -483,6 +483,7 @@ LV_ANIM = LvConstant(
LV_GRAD_DIR = LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER")
LV_DITHER = LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF")
LV_GRAD_EXTEND = LvConstant("LV_GRAD_EXTEND_", "PAD", "REPEAT", "REFLECT")
LV_LOG_LEVELS = {
"VERBOSE": "TRACE",
@@ -904,7 +905,7 @@ LV_COLOR_FORMATS = (
LV_DEFINES = (
"LV_USE_FREERTOS_TASK_NOTIFY", "LV_DRAW_BUF_STRIDE_ALIGN", "LV_USE_DRAW_SW", "LV_DRAW_SW_DRAW_UNIT_CNT",
"LV_DRAW_SW_COMPLEX", "LV_USE_DRAW_PXP", "LV_USE_PXP_DRAW_THREAD", "LV_USE_DRAW_G2D",
"LV_DRAW_SW_COMPLEX", "LV_USE_DRAW_SW_COMPLEX_GRADIENTS", "LV_USE_DRAW_PXP", "LV_USE_PXP_DRAW_THREAD", "LV_USE_DRAW_G2D",
"LV_USE_G2D_DRAW_THREAD", "LV_VG_LITE_USE_BOX_SHADOW", "LV_VG_LITE_THORVG_16PIXELS_ALIGN", "LV_LOG_USE_TIMESTAMP",
"LV_LOG_USE_FILE_LINE", "LV_USE_OBJ_ID_BUILTIN", "LV_USE_OBJ_PROPERTY_NAME", "LV_ATTRIBUTE_MEM_ALIGN_SIZE",
"LV_FONT_MONTSERRAT_14", "LV_USE_FONT_PLACEHOLDER", "LV_WIDGETS_HAS_DEFAULT_VALUE", "LV_USE_ARCLABEL",
+172 -23
View File
@@ -13,18 +13,40 @@ from esphome.core import ID
from esphome.cpp_generator import MockObj
from .defines import (
CONF_END_ANGLE,
CONF_GRADIENTS,
CONF_OPA,
CONF_START_ANGLE,
LV_DITHER,
LV_GRAD_EXTEND,
add_define,
add_lv_use,
add_warning,
)
from .lv_validation import lv_color, lv_percentage, opacity
from .lv_validation import (
lv_angle_degrees,
lv_color,
lv_percentage,
opacity,
pixels_or_percent,
)
from .lvcode import lv
from .types import lv_color_t, lv_gradient_t, lv_opa_t
CONF_STOPS = "stops"
CONF_LINEAR = "linear"
CONF_RADIAL = "radial"
CONF_CONICAL = "conical"
CONF_EXTEND = "extend"
CONF_FROM_X = "from_x"
CONF_FROM_Y = "from_y"
CONF_TO_X = "to_x"
CONF_TO_Y = "to_y"
CONF_CENTER_X = "center_x"
CONF_CENTER_Y = "center_y"
CONF_FOCAL_X = "focal_x"
CONF_FOCAL_Y = "focal_y"
CONF_FOCAL_RADIUS = "focal_radius"
def min_stops(value):
@@ -33,27 +55,109 @@ def min_stops(value):
return value
STOPS_SCHEMA = cv.All(
[
cv.Schema(
{
cv.Required(CONF_COLOR): lv_color,
cv.Optional(CONF_OPA, default=1.0): opacity,
cv.Required(CONF_POSITION): lv_percentage,
}
)
],
min_stops,
)
LINEAR_SCHEMA = cv.Schema(
{
cv.Required(CONF_FROM_X): pixels_or_percent,
cv.Required(CONF_FROM_Y): pixels_or_percent,
cv.Required(CONF_TO_X): pixels_or_percent,
cv.Required(CONF_TO_Y): pixels_or_percent,
cv.Optional(CONF_EXTEND, default="PAD"): LV_GRAD_EXTEND.one_of,
}
)
RADIAL_SCHEMA = cv.Schema(
{
cv.Required(CONF_CENTER_X): pixels_or_percent,
cv.Required(CONF_CENTER_Y): pixels_or_percent,
cv.Required(CONF_TO_X): pixels_or_percent,
cv.Required(CONF_TO_Y): pixels_or_percent,
cv.Optional(CONF_FOCAL_X): pixels_or_percent,
cv.Optional(CONF_FOCAL_Y): pixels_or_percent,
# No default: gradient_validator() must be able to tell whether this was actually
# given, to require it alongside focal_x/focal_y rather than silently drop it.
# LVGL's lv_grad_radial_set_focal() takes this as a scalar, not lv_pct() -
# unlike every other coordinate here, a percentage is not accepted.
cv.Optional(CONF_FOCAL_RADIUS): cv.positive_int,
cv.Optional(CONF_EXTEND, default="PAD"): LV_GRAD_EXTEND.one_of,
}
)
CONICAL_SCHEMA = cv.Schema(
{
cv.Required(CONF_CENTER_X): pixels_or_percent,
cv.Required(CONF_CENTER_Y): pixels_or_percent,
cv.Optional(CONF_START_ANGLE, default=0): lv_angle_degrees,
cv.Optional(CONF_END_ANGLE, default=360): lv_angle_degrees,
cv.Optional(CONF_EXTEND, default="PAD"): LV_GRAD_EXTEND.one_of,
}
)
def gradient_validator(config):
direction = config[CONF_DIRECTION]
for gradient_direction, key in (
("LINEAR", CONF_LINEAR),
("RADIAL", CONF_RADIAL),
("CONICAL", CONF_CONICAL),
):
if direction == gradient_direction:
if key not in config:
raise cv.Invalid(
f"'{key}' is required for {gradient_direction} gradient direction"
)
elif key in config:
raise cv.Invalid(
f"'{key}' is only valid with 'direction: {gradient_direction}'"
)
if CONF_RADIAL in config:
radial = config[CONF_RADIAL]
has_focal_x = CONF_FOCAL_X in radial
has_focal_y = CONF_FOCAL_Y in radial
has_focal_radius = CONF_FOCAL_RADIUS in radial
if has_focal_x != has_focal_y or (has_focal_radius and not has_focal_x):
raise cv.Invalid(
"'focal_x', 'focal_y' and 'focal_radius' must be specified together "
"in 'radial'"
)
return config
GRADIENT_SCHEMA = cv.ensure_list(
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t),
cv.Required(CONF_DIRECTION): cv.one_of(
"HOR", "HORIZONTAL", "VER", "VERTICAL", upper=True
),
cv.Optional(CONF_DITHER): LV_DITHER.one_of,
cv.Required(CONF_STOPS): cv.All(
[
cv.Schema(
{
cv.Required(CONF_COLOR): lv_color,
cv.Optional(CONF_OPA, default=1.0): opacity,
cv.Required(CONF_POSITION): lv_percentage,
}
)
],
min_stops,
),
}
cv.All(
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t),
cv.Required(CONF_DIRECTION): cv.one_of(
"HOR",
"HORIZONTAL",
"VER",
"VERTICAL",
"LINEAR",
"RADIAL",
"CONICAL",
upper=True,
),
cv.Optional(CONF_DITHER): LV_DITHER.one_of,
cv.Optional(CONF_LINEAR): LINEAR_SCHEMA,
cv.Optional(CONF_RADIAL): RADIAL_SCHEMA,
cv.Optional(CONF_CONICAL): CONICAL_SCHEMA,
cv.Required(CONF_STOPS): STOPS_SCHEMA,
}
),
gradient_validator,
)
)
@@ -65,15 +169,60 @@ async def gradients_to_code(config):
add_warning(
"The 'dither' option for gradients is not supported by LVGL 9.x and will be ignored"
)
if any(
x[CONF_DIRECTION] in ("LINEAR", "RADIAL", "CONICAL")
for x in config.get(CONF_GRADIENTS, ())
):
# LVGL's software renderer only draws these gradient types when this is enabled; without
# it they silently fall back to a plain horizontal gradient.
add_define("LV_USE_DRAW_SW_COMPLEX_GRADIENTS")
for gradient in config.get(CONF_GRADIENTS, ()):
var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->")
idbase = gradient[CONF_ID].id
stops = sorted(gradient[CONF_STOPS], key=itemgetter(CONF_POSITION))
max_stops = max(max_stops, len(stops))
if gradient[CONF_DIRECTION].startswith("VER"):
direction = gradient[CONF_DIRECTION]
if direction.startswith("VER"):
lv.grad_vertical_init(var)
else:
elif direction.startswith("HOR"):
lv.grad_horizontal_init(var)
elif direction == "LINEAR":
linear = gradient[CONF_LINEAR]
lv.grad_linear_init(
var,
await pixels_or_percent.process(linear[CONF_FROM_X]),
await pixels_or_percent.process(linear[CONF_FROM_Y]),
await pixels_or_percent.process(linear[CONF_TO_X]),
await pixels_or_percent.process(linear[CONF_TO_Y]),
await LV_GRAD_EXTEND.process(linear[CONF_EXTEND]),
)
elif direction == "RADIAL":
radial = gradient[CONF_RADIAL]
lv.grad_radial_init(
var,
await pixels_or_percent.process(radial[CONF_CENTER_X]),
await pixels_or_percent.process(radial[CONF_CENTER_Y]),
await pixels_or_percent.process(radial[CONF_TO_X]),
await pixels_or_percent.process(radial[CONF_TO_Y]),
await LV_GRAD_EXTEND.process(radial[CONF_EXTEND]),
)
if CONF_FOCAL_X in radial:
lv.grad_radial_set_focal(
var,
await pixels_or_percent.process(radial[CONF_FOCAL_X]),
await pixels_or_percent.process(radial[CONF_FOCAL_Y]),
radial.get(CONF_FOCAL_RADIUS, 0),
)
elif direction == "CONICAL":
conical = gradient[CONF_CONICAL]
lv.grad_conical_init(
var,
await pixels_or_percent.process(conical[CONF_CENTER_X]),
await pixels_or_percent.process(conical[CONF_CENTER_Y]),
await lv_angle_degrees.process(conical[CONF_START_ANGLE]),
await lv_angle_degrees.process(conical[CONF_END_ANGLE]),
await LV_GRAD_EXTEND.process(conical[CONF_EXTEND]),
)
stop_colors = cg.static_const_array(
ID(idbase + "_colors_", type=lv_color_t),
[await lv_color.process(x[CONF_COLOR]) for x in stops],
+65
View File
@@ -209,6 +209,63 @@ lvgl:
position: 212
- color: 0xFF0000
position: 255
- id: linear_grad
direction: LINEAR
linear:
from_x: 0%
from_y: 0%
to_x: 100%
to_y: 0%
extend: REFLECT
stops:
- color: 0xFF0000
position: 0
- color: 0x0000FF
position: 255
- id: radial_grad
direction: RADIAL
radial:
center_x: 50%
center_y: 50%
to_x: 100%
to_y: 50%
extend: PAD
stops:
- color: 0xFFFFFF
position: 0
- color: 0x000000
position: 255
- id: radial_focal_grad
direction: RADIAL
radial:
center_x: 50%
center_y: 50%
to_x: 100%
to_y: 50%
focal_x: 40%
focal_y: 40%
focal_radius: 10
extend: REPEAT
stops:
- color: 0xFF0000
position: 0
- color: 0x0000FF
position: 255
- id: conical_grad
direction: CONICAL
conical:
center_x: 50%
center_y: 50%
start_angle: 0
end_angle: 360
extend: PAD
stops:
- color: 0xFF0000
position: 0
- color: 0x00FF00
position: 127
- color: 0xFF0000
position: 255
style_definitions:
- id: style_test
@@ -1070,6 +1127,14 @@ lvgl:
logger.log:
format: Slider released at %d/%d with value %.0f
args: ['(int) point.x', '(int) point.y', x]
# Exercises the style-application path for a complex gradient, not just its
# lv_grad_*_init() codegen: the other new gradients are only ever declared.
- obj:
bg_opa: cover
bg_grad: conical_grad
width: 40
height: 40
- button:
styles: spin_button
id: spin_up