Updated icons, buttons, and display; issues still with Main Room button, and sliders.
This commit is contained in:
1 parent
b5d6b4a897
commit
45998b6176
15 files changed
+973
-77
No files matched your search
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate the icon font the UI uses: main/ui/icon_font.c + main/ui/ui_icons.h.
|
||||
|
||||
The icons are a ~30-glyph subset of Material Design Icons (the same icon set
|
||||
Home Assistant uses, so the panel and the HA dashboard agree on what "partly
|
||||
cloudy" looks like). The subset TTF is embedded as a C array and rasterised at
|
||||
runtime by LVGL's tiny_ttf, which means:
|
||||
|
||||
- icons are just label text in a special font -- colourable, sizeable,
|
||||
no per-icon bitmaps to regenerate when a size changes;
|
||||
- the whole set costs a few KB of flash, not a 1.3MB font.
|
||||
|
||||
Inputs (cached copies are fine; only regeneration needs them):
|
||||
|
||||
curl -sfL -o /tmp/mdi.css https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css
|
||||
curl -sfL -o /tmp/mdi.ttf https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/fonts/materialdesignicons-webfont.ttf
|
||||
python3 tools/make_icon_font.py /tmp/mdi.css /tmp/mdi.ttf
|
||||
|
||||
Codepoints are resolved from the CSS at generation time, never hardcoded --
|
||||
MDI has re-assigned codepoints between major versions, and a stale codepoint
|
||||
renders as the wrong icon with no error anywhere.
|
||||
|
||||
The generated files are committed, so a normal build needs neither network
|
||||
nor fonttools.
|
||||
"""
|
||||
|
||||
import io
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from fontTools import subset
|
||||
except ImportError:
|
||||
sys.exit("fonttools is required: python3 -m venv /tmp/fontenv && "
|
||||
"/tmp/fontenv/bin/pip install fonttools (then run with that python)")
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
OUT_C = REPO / "main" / "ui" / "icon_font.c"
|
||||
OUT_H = REPO / "main" / "ui" / "ui_icons.h"
|
||||
|
||||
# macro name -> candidate MDI icon names, first match wins. Candidates exist
|
||||
# because MDI occasionally renames icons between versions.
|
||||
ICONS = {
|
||||
# met.no condition slugs (see ha_condition_* in ha_data.c)
|
||||
"ICON_W_SUNNY": ["weather-sunny"],
|
||||
"ICON_W_NIGHT": ["weather-night"],
|
||||
"ICON_W_PARTLY": ["weather-partly-cloudy"],
|
||||
"ICON_W_CLOUDY": ["weather-cloudy"],
|
||||
"ICON_W_RAINY": ["weather-rainy"],
|
||||
"ICON_W_POURING": ["weather-pouring"],
|
||||
"ICON_W_LIGHTNING": ["weather-lightning"],
|
||||
"ICON_W_LIGHTNING_RAIN":["weather-lightning-rainy"],
|
||||
"ICON_W_SNOWY": ["weather-snowy"],
|
||||
"ICON_W_SNOWY_RAIN": ["weather-snowy-rainy"],
|
||||
"ICON_W_FOG": ["weather-fog"],
|
||||
"ICON_W_WINDY": ["weather-windy"],
|
||||
"ICON_W_HAIL": ["weather-hail"],
|
||||
"ICON_W_ALERT": ["alert-circle-outline"],
|
||||
|
||||
# grid cells
|
||||
"ICON_THERMOMETER": ["thermometer"],
|
||||
"ICON_HOME_THERMO": ["home-thermometer-outline", "home-thermometer"],
|
||||
"ICON_WATER": ["water-outline", "water"],
|
||||
"ICON_SUNRISE": ["weather-sunset-up"],
|
||||
"ICON_SUNSET": ["weather-sunset-down"],
|
||||
"ICON_GAUGE": ["gauge"],
|
||||
|
||||
# light tiles (order-independent; ha_config.h references these by name)
|
||||
"ICON_SOFA": ["sofa-outline", "sofa"],
|
||||
"ICON_DINING": ["silverware-fork-knife", "silverware"],
|
||||
"ICON_FLOOR_LAMP": ["floor-lamp"],
|
||||
"ICON_DESK_LAMP": ["desk-lamp"],
|
||||
"ICON_CHANDELIER": ["chandelier", "ceiling-light"],
|
||||
"ICON_STRING_LIGHTS": ["string-lights", "outdoor-lamp"],
|
||||
}
|
||||
|
||||
|
||||
def resolve_codepoints(css: str) -> dict[str, int]:
|
||||
"""macro -> unicode codepoint, via the .mdi-<name>::before rules."""
|
||||
cp_by_name: dict[str, int] = {}
|
||||
for m in re.finditer(r'\.mdi-([a-z0-9-]+)::?before\{content:"\\([0-9A-Fa-f]+)"\}', css):
|
||||
cp_by_name[m.group(1)] = int(m.group(2), 16)
|
||||
|
||||
if not cp_by_name:
|
||||
sys.exit("No codepoint rules found in the CSS -- wrong file?")
|
||||
|
||||
resolved: dict[str, int] = {}
|
||||
for macro, candidates in ICONS.items():
|
||||
for name in candidates:
|
||||
if name in cp_by_name:
|
||||
resolved[macro] = cp_by_name[name]
|
||||
print(f" {macro:<24} mdi-{name:<24} U+{cp_by_name[name]:05X}")
|
||||
break
|
||||
else:
|
||||
sys.exit(f"None of {candidates} exist in this MDI release "
|
||||
f"(wanted for {macro}) -- pick another candidate")
|
||||
return resolved
|
||||
|
||||
|
||||
def subset_font(ttf_path: Path, codepoints: list[int]) -> bytes:
|
||||
opts = subset.Options()
|
||||
opts.flavor = None # plain TTF out
|
||||
opts.desubroutinize = True
|
||||
font = subset.load_font(str(ttf_path), opts)
|
||||
ss = subset.Subsetter(opts)
|
||||
ss.populate(unicodes=codepoints)
|
||||
ss.subset(font)
|
||||
buf = io.BytesIO()
|
||||
font.save(buf)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def utf8_escape(cp: int) -> str:
|
||||
return "".join(f"\\x{b:02x}" for b in chr(cp).encode("utf-8"))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 3:
|
||||
sys.exit(__doc__)
|
||||
css_path, ttf_path = Path(sys.argv[1]), Path(sys.argv[2])
|
||||
|
||||
print("resolving codepoints from CSS:")
|
||||
resolved = resolve_codepoints(css_path.read_text())
|
||||
|
||||
data = subset_font(ttf_path, sorted(resolved.values()))
|
||||
print(f"\nsubset TTF: {len(data)} bytes ({len(resolved)} glyphs)")
|
||||
|
||||
guard_note = (
|
||||
"/*\n"
|
||||
" * GENERATED by tools/make_icon_font.py -- do not edit by hand.\n"
|
||||
" * Subset of Material Design Icons (Apache-2.0), rasterised at runtime\n"
|
||||
" * by LVGL tiny_ttf. Regeneration instructions are in the script.\n"
|
||||
" */\n"
|
||||
)
|
||||
|
||||
lines = [guard_note, "#include <stddef.h>\n"]
|
||||
lines.append(f"const size_t icon_font_data_size = {len(data)};\n")
|
||||
lines.append("const unsigned char icon_font_data[] = {")
|
||||
for i in range(0, len(data), 16):
|
||||
chunk = ", ".join(f"0x{b:02x}" for b in data[i:i + 16])
|
||||
lines.append(f" {chunk},")
|
||||
lines.append("};\n")
|
||||
OUT_C.write_text("\n".join(lines))
|
||||
|
||||
h = [guard_note]
|
||||
h.append("#ifndef UI_ICONS_H")
|
||||
h.append("#define UI_ICONS_H\n")
|
||||
h.append("#include <stddef.h>\n")
|
||||
h.append("/* The subset TTF; feed to lv_tiny_ttf_create_data(). */")
|
||||
h.append("extern const unsigned char icon_font_data[];")
|
||||
h.append("extern const size_t icon_font_data_size;\n")
|
||||
h.append("/* Each icon is a single glyph as a UTF-8 string -- usable as label text")
|
||||
h.append(" * in any font created from icon_font_data. */")
|
||||
for macro, cp in resolved.items():
|
||||
h.append(f'#define {macro:<24} "{utf8_escape(cp)}" /* U+{cp:05X} */')
|
||||
h.append("\n#endif")
|
||||
OUT_H.write_text("\n".join(h) + "\n")
|
||||
|
||||
print(f"wrote {OUT_C.relative_to(REPO)}")
|
||||
print(f"wrote {OUT_H.relative_to(REPO)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user