Custom icon per tile: 150 MDI icons in firmware and editor
- screen_manager/app/tile_icons.py is the single icon list (12 groups, 20 media and music icons); tools/generate_icons.py writes it into the three icon fonts of both boards and builds the editor's subset font. - Firmware 0.2.18 shows o.icon when its fonts carry the glyph, else the domain icon. The app sends the chosen icon, or Home Assistant's own mdi icon on auto. - Tile sheet gets an icon picker with search; the mockup shows the icon the screen actually draws. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,7 @@ struct Tile {
|
||||
float battery = NAN, volume = NAN;
|
||||
uint32_t supported = 0, background = 0;
|
||||
bool transparent = false; // "Achtergrond: geen": card fill and border hidden, contents unchanged.
|
||||
std::string icon; // UTF-8 glyph of a chosen icon the icon fonts contain; empty keeps the domain icon.
|
||||
std::string revision, pending_revision;
|
||||
uint32_t pending_since = 0;
|
||||
bool pending = false, confirmed = false, local_feedback = false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "runtime_model.h"
|
||||
#include "tile_palette.h"
|
||||
#include "tile_icon.h"
|
||||
#include "screen_settings.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "cyd_ui.h"
|
||||
@@ -81,6 +82,11 @@ struct Widgets {
|
||||
};
|
||||
constexpr unsigned POINT_BUFFER = 128;
|
||||
inline std::array<Widgets, 10> widgets;
|
||||
// All icon fonts carry the same generated glyph set, so the first bound one answers for all.
|
||||
inline bool has_icon_glyph(uint32_t codepoint) {
|
||||
for (auto &w : widgets) if (w.icon_font) { lv_font_glyph_dsc_t dsc; return lv_font_get_glyph_dsc(w.icon_font, &dsc, codepoint, 0); }
|
||||
return false;
|
||||
}
|
||||
inline bool fresh() { return model.ready() && esphome::millis() - last_received < 95000; }
|
||||
inline float number(JsonVariant value, float fallback = NAN) {
|
||||
if (!value.is<float>() && !value.is<int>()) return fallback;
|
||||
@@ -164,6 +170,9 @@ inline std::string receive(const std::string &payload) {
|
||||
std::string background=string(options["background"],16);
|
||||
tile.background = tile_palette::color(background);
|
||||
tile.transparent = tile_palette::transparent(background);
|
||||
// An icon these fonts lack (a newer set than this firmware) keeps the domain icon.
|
||||
uint32_t icon = tile_icon::codepoint(string(options["icon"], 8));
|
||||
tile.icon = icon && has_icon_glyph(icon) ? tile_icon::utf8(icon) : "";
|
||||
tile.tap = string(options["tap"]); if (tile.tap.empty()) tile.tap="auto";
|
||||
tile.display = string(options["display"]); if (tile.display.empty()) tile.display="standard";
|
||||
tile.inline_control = string(options["inline"]); if (tile.inline_control.empty()) tile.inline_control="none";
|
||||
@@ -512,6 +521,7 @@ inline const char *weather_text(const std::string &condition) {
|
||||
return condition.c_str();
|
||||
}
|
||||
inline const char *icon_for(const Tile &tile) {
|
||||
if (!tile.icon.empty()) return tile.icon.c_str();
|
||||
auto d = tile.domain();
|
||||
if (d == "light") return "\U000F0335";
|
||||
if (d == "climate") return "\U000F001B";
|
||||
@@ -790,7 +800,7 @@ inline void render_forecast(Widgets &w,const Tile &t,bool large,int width,int he
|
||||
int left=large?150:96,icon_h=lv_font_get_line_height(w.icon_font),temp_h=lv_font_get_line_height(temp_font),text_h=lv_font_get_line_height(w.value_font);
|
||||
int block=std::max(icon_h,temp_h)+2+text_h,y=std::max(0,(height-block)/2);
|
||||
char b[24];snprintf(b,sizeof(b),"%.0f°",t.current);
|
||||
auto *icon=part_label(w,0,w.icon_font,0,y+(std::max(icon_h,temp_h)-icon_h)/2,icon_h+4,LV_TEXT_ALIGN_LEFT,icon_for(t));
|
||||
auto *icon=part_label(w,0,w.icon_font,0,y+(std::max(icon_h,temp_h)-icon_h)/2,icon_h+4,LV_TEXT_ALIGN_LEFT,t.available()?weather_icon(t.state):"\U000F0595");
|
||||
lv_obj_set_width(icon,lv_font_get_line_height(w.icon_font)+4);
|
||||
part_label(w,1,temp_font,icon_h+6,y+(std::max(icon_h,temp_h)-temp_h)/2,left-icon_h-6,LV_TEXT_ALIGN_LEFT,std::isfinite(t.current)?b:"");
|
||||
part_label(w,2,w.value_font,0,y+std::max(icon_h,temp_h)+2,left-4,LV_TEXT_ALIGN_LEFT,weather_text(t.state));
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace tile_icon {
|
||||
// ESP Screen Manager sends a chosen icon as its Material Design Icons codepoint
|
||||
// in hex ("F06B5"). Anything else is 0: the tile keeps its domain icon.
|
||||
inline uint32_t codepoint(const std::string &hex) {
|
||||
if (hex.size() != 5) return 0;
|
||||
uint32_t value = 0;
|
||||
for (char c : hex) {
|
||||
int digit = c >= '0' && c <= '9' ? c - '0' : c >= 'A' && c <= 'F' ? c - 'A' + 10 : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1;
|
||||
if (digit < 0) return 0;
|
||||
value = value << 4 | static_cast<uint32_t>(digit);
|
||||
}
|
||||
// MDI lives in Supplementary Private Use Area-A.
|
||||
return value >= 0xF0000 && value <= 0xFFFFD ? value : 0;
|
||||
}
|
||||
// Four-byte UTF-8 for a label; empty for 0.
|
||||
inline std::string utf8(uint32_t cp) {
|
||||
if (cp < 0x10000 || cp > 0x10FFFF) return {};
|
||||
return {static_cast<char>(0xF0 | cp >> 18), static_cast<char>(0x80 | (cp >> 12 & 0x3F)),
|
||||
static_cast<char>(0x80 | (cp >> 6 & 0x3F)), static_cast<char>(0x80 | (cp & 0x3F))};
|
||||
}
|
||||
} // namespace tile_icon
|
||||
Reference in New Issue
Block a user