e37148f9b1
Every screen gets the user action esphome.<device>_show_alert with title, subtitle, icon, color, button_text, timeout and flash. The card sits on LVGL's top layer above every page, card and the dim overlay, wakes the screen and keeps the backlight at its normal brightness until the button (or the timeout; 0 waits for the button, the button always closes at once). flash: true blinks the backlight four times on arrival. Icons are the tile picker names (also mdi:name or the hex codepoint of a compiled glyph, else alert-outline), colours the tile pastels. A new alert replaces the current one; every ending fires esphome.screen_alert with action, title and screen. dismiss_alert removes the card remotely. The field is button_text rather than button: ESPHome reserves the names of loaded components as ids and the CYD profile loads button:. The C++ namespace is screen_alert because the widget id alert_overlay becomes a global lv_obj_t*. tools/generate_icons.py now also writes tile_icon_names.h (name to codepoint) from the one icon list. Firmware 0.2.31 for both boards. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
130 lines
5.8 KiB
Python
130 lines
5.8 KiB
Python
"""Write the tile icon set into both board profiles and build the editor's icon font.
|
|
|
|
The list lives in screen_manager/app/tile_icons.py. Every Material Design Icons
|
|
font of a board profile gets exactly those glyphs (the first font carries the
|
|
list as a YAML anchor, the smaller sizes reuse it), and the editor gets a subset
|
|
of the same TTF so its mockup shows the glyphs the screen draws. Run
|
|
tools/generate_packages.py afterwards; --check changes nothing and fails when
|
|
anything is out of date.
|
|
"""
|
|
import argparse
|
|
import io
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from fontTools import subset
|
|
from fontTools.ttLib import TTFont
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / 'screen_manager/app'))
|
|
import tile_icons # noqa: E402
|
|
|
|
TTF = ROOT / 'fonts/materialdesignicons-webfont.ttf'
|
|
PROFILES = [ROOT / 'home-like-2432s028.yaml', ROOT / 'guition-4848s040.yaml']
|
|
WEB_FONT = ROOT / 'screen_manager/app/static/tile-icons.woff'
|
|
NAME_TABLE = ROOT / 'components/smart_display/tile_icon_names.h'
|
|
FONT_BLOCK = re.compile(r"( - file: 'fonts/materialdesignicons-webfont\.ttf'\n id: \w+\n size: \d+\n) glyphs:.*\n(?: .*\n)*")
|
|
|
|
def verify():
|
|
"""Names and codepoints must match the TTF the firmware is built from."""
|
|
codepoints = {}
|
|
for code, name in TTFont(TTF).getBestCmap().items():
|
|
codepoints.setdefault(name, code)
|
|
codepoints[name] = min(codepoints[name], code)
|
|
seen = set()
|
|
for name, code in tile_icons.GLYPHS.items():
|
|
if codepoints.get(name) != int(code, 16):
|
|
raise SystemExit(f'{name}: codepoint {code} does not match {TTF.name}')
|
|
if code in seen:
|
|
raise SystemExit(f'Duplicate glyph {code}')
|
|
seen.add(code)
|
|
if len(tile_icons.ICONS) != sum(len(icons) for _, icons in tile_icons.GROUPS):
|
|
raise SystemExit('An icon appears in more than one group')
|
|
|
|
def glyph_list():
|
|
lines = [' glyphs: &tile_icons # GENERATED by tools/generate_icons.py from screen_manager/app/tile_icons.py']
|
|
lines += [f' - "\\U000{code}" # {name}' for name, code in tile_icons.GLYPHS.items()]
|
|
return '\n'.join(lines) + '\n'
|
|
|
|
def profile(text):
|
|
blocks = list(FONT_BLOCK.finditer(text))
|
|
if len(blocks) != 3:
|
|
raise SystemExit(f'Expected three icon fonts, found {len(blocks)}')
|
|
out, last = [], 0
|
|
for index, block in enumerate(blocks):
|
|
out += [text[last:block.start()], block[1], glyph_list() if index == 0 else ' glyphs: *tile_icons\n']
|
|
last = block.end()
|
|
return ''.join(out) + text[last:]
|
|
|
|
def name_table():
|
|
"""Name -> codepoint for the firmware, so a Home Assistant action can say icon: doorbell."""
|
|
lines = ['#pragma once',
|
|
'// GENERATED by tools/generate_icons.py from screen_manager/app/tile_icons.py; do not edit.',
|
|
'#include <cstdint>', '#include <string>', '', 'namespace tile_icon {',
|
|
'struct Named { const char *name; uint32_t codepoint; };',
|
|
'// Every glyph the three icon fonts of both boards carry, by Material Design Icons name.',
|
|
'inline constexpr Named NAMES[] = {']
|
|
lines += [f' {{"{name}", 0x{code}}},' for name, code in tile_icons.GLYPHS.items()]
|
|
lines += ['};',
|
|
'inline constexpr size_t NAME_COUNT = sizeof(NAMES) / sizeof(NAMES[0]);',
|
|
'// Codepoint of a name the fonts carry; 0 for any other name.',
|
|
'inline uint32_t named(const std::string &name) {',
|
|
' for (const auto &entry : NAMES) if (name == entry.name) return entry.codepoint;',
|
|
' return 0;',
|
|
'}',
|
|
'// True when the fonts carry this codepoint, so a label can draw it.',
|
|
'inline bool carried(uint32_t codepoint) {',
|
|
' for (const auto &entry : NAMES) if (entry.codepoint == codepoint) return true;',
|
|
' return false;',
|
|
'}',
|
|
'} // namespace tile_icon']
|
|
return '\n'.join(lines) + '\n'
|
|
|
|
def web_font():
|
|
font = TTFont(TTF)
|
|
options = subset.Options()
|
|
options.flavor = 'woff'
|
|
options.layout_features = []
|
|
options.name_IDs = []
|
|
options.notdef_outline = False
|
|
subsetter = subset.Subsetter(options)
|
|
subsetter.populate(unicodes=[int(code, 16) for code in tile_icons.GLYPHS.values()])
|
|
subsetter.subset(font)
|
|
# The source stores xMin 0 with left bearing 0; saving recalculates xMin, and browsers
|
|
# then shift each glyph left by the difference. Keep the bearing equal to the real xMin.
|
|
glyf, hmtx = font['glyf'], font['hmtx']
|
|
for name in font.getGlyphOrder():
|
|
glyph = glyf[name]
|
|
glyph.recalcBounds(glyf)
|
|
hmtx[name] = (hmtx[name][0], getattr(glyph, 'xMin', 0))
|
|
buffer = io.BytesIO()
|
|
font.flavor = 'woff'
|
|
font.save(buffer)
|
|
return buffer.getvalue()
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--check', action='store_true')
|
|
args = parser.parse_args()
|
|
verify()
|
|
for path in PROFILES:
|
|
expected = profile(path.read_text())
|
|
if args.check:
|
|
if path.read_text() != expected: raise SystemExit(f'Outdated icon fonts: {path.name}')
|
|
else:
|
|
path.write_text(expected)
|
|
if args.check:
|
|
if not NAME_TABLE.exists() or NAME_TABLE.read_text() != name_table(): raise SystemExit(f'Outdated: {NAME_TABLE.name}')
|
|
# Compare glyph coverage, not bytes: font output differs between fontTools versions.
|
|
wanted = {int(code, 16) for code in tile_icons.GLYPHS.values()}
|
|
if not WEB_FONT.exists() or set(TTFont(WEB_FONT).getBestCmap()) != wanted:
|
|
raise SystemExit(f'Outdated: {WEB_FONT.name}')
|
|
else:
|
|
NAME_TABLE.write_text(name_table())
|
|
WEB_FONT.write_bytes(web_font())
|
|
print(f'{len(tile_icons.ICONS)} pickable icons, {len(tile_icons.GLYPHS)} glyphs: ' + ('verified' if args.check else 'generated'))
|
|
|
|
if __name__ == '__main__':
|
|
main()
|