Files
esphome/esphome/components/lvgl/widgets/dropdown.py
Copilot 29555c0ddc [lvgl] Validate LVGL dropdown symbols require Unicode codepoint ≥ 0x100 (#13394)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-21 12:32:55 -05:00

123 lines
4.2 KiB
Python

from esphome import codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_OPTIONS
from ..defines import (
CONF_DIR,
CONF_INDICATOR,
CONF_MAIN,
CONF_SCROLLBAR,
CONF_SELECTED,
CONF_SELECTED_INDEX,
CONF_SELECTED_TEXT,
CONF_SYMBOL,
DIRECTIONS,
literal,
)
from ..helpers import lvgl_components_required
from ..lv_validation import lv_int, lv_text, option_string
from ..lvcode import LocalVariable, lv, lv_add, lv_expr
from ..schemas import part_schema
from ..types import LvCompound, LvSelect, LvType, lv_obj_t
from . import Widget, WidgetType, set_obj_properties
from .label import CONF_LABEL
CONF_DROPDOWN = "dropdown"
CONF_DROPDOWN_LIST = "dropdown_list"
# Example valid dropdown symbol (left arrow) for error messages
EXAMPLE_DROPDOWN_SYMBOL = "\U00002190" # ←
def dropdown_symbol_validator(value):
"""
Validate that the dropdown symbol is a single Unicode character
with a codepoint of 0x100 (256) or greater.
This is required because LVGL uses codepoints below 0x100 for internal symbols.
"""
value = cv.string(value)
# len(value) counts Unicode code points, not grapheme clusters or bytes
if len(value) != 1:
raise cv.Invalid(
f"Dropdown symbol must be a single character, got '{value}' with length {len(value)}"
)
codepoint = ord(value)
if codepoint < 0x100:
# Format the example symbol as a Unicode escape for the error message
example_escape = f"\\U{ord(EXAMPLE_DROPDOWN_SYMBOL):08X}"
raise cv.Invalid(
f"Dropdown symbol must have a Unicode codepoint of 0x100 (256) or greater. "
f"'{value}' has codepoint {codepoint} (0x{codepoint:X}). "
f"Use a character like '{example_escape}' ({EXAMPLE_DROPDOWN_SYMBOL}) or other Unicode symbols with codepoint >= 0x100."
)
return value
lv_dropdown_t = LvSelect("LvDropdownType", parents=(LvCompound,))
lv_dropdown_list_t = LvType("lv_dropdown_list_t")
dropdown_list_spec = WidgetType(
CONF_DROPDOWN_LIST, lv_dropdown_list_t, (CONF_MAIN, CONF_SELECTED, CONF_SCROLLBAR)
)
DROPDOWN_BASE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_SYMBOL): dropdown_symbol_validator,
cv.Exclusive(CONF_SELECTED_INDEX, CONF_SELECTED_TEXT): lv_int,
cv.Exclusive(CONF_SELECTED_TEXT, CONF_SELECTED_TEXT): lv_text,
cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec.parts),
}
)
DROPDOWN_SCHEMA = DROPDOWN_BASE_SCHEMA.extend(
{
cv.Required(CONF_OPTIONS): cv.ensure_list(option_string),
cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of,
}
)
DROPDOWN_UPDATE_SCHEMA = DROPDOWN_BASE_SCHEMA.extend(
{
cv.Optional(CONF_OPTIONS): cv.ensure_list(option_string),
cv.Optional(CONF_DIR): DIRECTIONS.one_of,
}
)
class DropdownType(WidgetType):
def __init__(self):
super().__init__(
CONF_DROPDOWN,
lv_dropdown_t,
(CONF_MAIN, CONF_INDICATOR),
DROPDOWN_SCHEMA,
modify_schema=DROPDOWN_UPDATE_SCHEMA,
)
async def to_code(self, w: Widget, config):
lvgl_components_required.add(CONF_DROPDOWN)
if options := config.get(CONF_OPTIONS):
lv_add(w.var.set_options(options))
if symbol := config.get(CONF_SYMBOL):
lv.dropdown_set_symbol(w.var.obj, cg.safe_exp(symbol))
if (selected := config.get(CONF_SELECTED_INDEX)) is not None:
value = await lv_int.process(selected)
lv_add(w.var.set_selected_index(value, literal("LV_ANIM_OFF")))
if (selected := config.get(CONF_SELECTED_TEXT)) is not None:
value = await lv_text.process(selected)
lv_add(w.var.set_selected_text(value, literal("LV_ANIM_OFF")))
if dirn := config.get(CONF_DIR):
lv.dropdown_set_dir(w.obj, literal(dirn))
if dlist := config.get(CONF_DROPDOWN_LIST):
with LocalVariable(
"dropdown_list", lv_obj_t, lv_expr.dropdown_get_list(w.obj)
) as dlist_obj:
dwid = Widget(dlist_obj, dropdown_list_spec, dlist)
await set_obj_properties(dwid, dlist)
def get_uses(self):
return (CONF_LABEL,)
dropdown_spec = DropdownType()