#pragma once #include #include #include #include namespace header_bar { // The top bar right of the screen name (firmware 0.2.32+). ESP Screen Manager decides what shows // and writes an entity's text; the screen draws it, ticks its clocks and counts "5 min ago" // itself. Everything here is free of LVGL, so tests/test_header_bar.cpp covers it on a PC. constexpr size_t MAX_ITEMS = 6; constexpr size_t TEXT_BYTES = 48; enum class Kind : uint8_t { none, clock, analog, date, text, ago }; inline Kind kind(const std::string &name) { if (name == "clock") return Kind::clock; if (name == "analog") return Kind::analog; if (name == "date") return Kind::date; if (name == "text") return Kind::text; if (name == "ago") return Kind::ago; return Kind::none; } struct Item { Kind kind = Kind::none; uint32_t icon = 0; // Material Design Icons codepoint, 0 without an icon std::string text; // Kind::text int64_t epoch = 0; // Kind::ago: the moment, past or future uint32_t color = 0; // accent of the icon while `has_color` bool has_color = false; bool operator==(const Item &o) const { return kind == o.kind && icon == o.icon && text == o.text && epoch == o.epoch && color == o.color && has_color == o.has_color; } }; struct Bar { std::array items{}; size_t count = 0; bool received = false; // false until the manager sent one: the clock of show_clock then }; // "RRGGBB" as 0xRRGGBB; false for anything else. inline bool color(const std::string &hex, uint32_t &out) { if (hex.size() != 6) return false; 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 false; value = value << 4 | static_cast(digit); } out = value; return true; } // Next codepoint of UTF-8 text at `i` (advanced past it); 0 at the end. Broken bytes count as one. inline uint32_t next_codepoint(const std::string &s, size_t &i) { if (i >= s.size()) return 0; unsigned char c = static_cast(s[i]); int extra = c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 0; uint32_t cp = extra == 3 ? c & 0x07 : extra == 2 ? c & 0x0F : extra == 1 ? c & 0x1F : c; ++i; for (int k = 0; k < extra && i < s.size() && (static_cast(s[i]) & 0xC0) == 0x80; ++k, ++i) cp = cp << 6 | (static_cast(s[i]) & 0x3F); return cp; } // Relative time in the editor's words (app.js agoText): "Just now", "5 min ago", "Yesterday", // "In 2 hours". `now` 0 means the clock is not set yet. inline std::string ago_text(int64_t then, int64_t now) { if (now <= 0 || then <= 0) return "—"; int64_t seconds = now - then, span = seconds < 0 ? -seconds : seconds; auto n = [&](int64_t unit) { return std::to_string(span / unit); }; if (seconds < 0) { if (span < 3600) return "In " + std::to_string(std::max(1, span / 60)) + " min"; if (span < 86400) return span / 3600 == 1 ? "In 1 hour" : "In " + n(3600) + " hours"; if (span < 172800) return "Tomorrow"; return "In " + n(86400) + " days"; } if (span < 60) return "Just now"; if (span < 3600) return n(60) + " min ago"; if (span < 86400) return span / 3600 == 1 ? "1 hour ago" : n(3600) + " hours ago"; if (span < 172800) return "Yesterday"; if (span < 604800) return n(86400) + " days ago"; if (span < 2592000) return span / 604800 == 1 ? "1 week ago" : n(604800) + " weeks ago"; if (span < 31536000) return span / 2592000 == 1 ? "1 month ago" : n(2592000) + " months ago"; return span / 31536000 == 1 ? "1 year ago" : n(31536000) + " years ago"; } // "Mo 14 Sep"; day_of_week 1 is Sunday, as ESPHome counts. inline std::string date_text(int day_of_week, int day_of_month, int month) { static const char *days[] = {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"}; static const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; if (day_of_week < 1 || day_of_week > 7 || month < 1 || month > 12) return "—"; return std::string(days[day_of_week - 1]) + " " + std::to_string(day_of_month) + " " + months[month - 1]; } // What you see between icon and value, between two items and after the name, from the height of // the digits; the editor's barGaps() uses the same factors. struct Gaps { int icon, item, name; }; inline int scaled(int cap, int tenths) { return (cap * tenths + 5) / 10; } inline Gaps gaps(int cap) { return {std::max(2, scaled(cap, 4)), std::max(6, (cap * 125 + 50) / 100), std::max(8, scaled(cap, 16))}; } // Right-aligned placement of item widths (ink, gaps included) in `width` pixels beside a name of // `name_natural` pixels. The name keeps at least min(natural, 35% of the width); items leave from // the front until the rest fits. `x` is each item's left ink edge; `name_room` what the name may use. struct Placement { size_t first = 0; std::array x{}; int name_room = 0; }; inline Placement place(const int *widths, size_t count, const Gaps &g, int width, int name_natural) { Placement p; count = std::min(count, MAX_ITEMS); int min_name = std::min(name_natural, width * 35 / 100); auto total = [&](size_t from) { int sum = 0; for (size_t i = from; i < count; ++i) sum += widths[i] + (i > from ? g.item : 0); return sum; }; while (p.first < count && total(p.first) + g.name + min_name > width) ++p.first; int x = width - total(p.first); for (size_t i = p.first; i < count; ++i) { p.x[i] = x; x += widths[i] + g.item; } p.name_room = p.first < count ? p.x[p.first] - g.name : width; return p; } } // namespace header_bar