3e959b3df9
Everything a user sees or reads is now English: both boards' labels, status replies and logs, the ESP Screens editor and Alerts cheatsheet, and the README and guides (docs/TILES.md, CALIBRATING.md, ACCEPTANCE.md and TROUBLESHOOTING.md replace the Dutch file names). Labels follow Home Assistant's own English names (Heat/Cool modes, Heating/Cooling actions, Away, Returning to dock, weather conditions), dates use English abbreviations and the top bar writes numbers the English way. Docs use HA 2026.9's menu names (Settings -> Apps, App store, Settings -> Tools). Unused legacy files (home-like.yaml, buttons.yaml, a.out) are removed. Firmware 0.2.34 names its entities in English. Home Assistant registers a renamed ESPHome entity under a new entity ID, so the inbox text.<screen>_tegelinstellingen becomes text.<screen>_tile_settings and the layout stored under the old ID would be lost. The manager now recognises the screen by its device, during a run or after a restart, and moves its layout and update history along; the Update button and the nightly round follow the screen through the rename. Screens that still run 0.2.33 or older keep working: both languages are matched. Main is the only release branch from now on. Tested on a Home Assistant Yellow (HA 2026.9.1) with both bench boards: see docs/TEST_RESULTS_0240.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
129 lines
5.6 KiB
C++
129 lines
5.6 KiB
C++
#pragma once
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <string>
|
|
|
|
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<Item, MAX_ITEMS> 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<uint32_t>(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<unsigned char>(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<unsigned char>(s[i]) & 0xC0) == 0x80; ++k, ++i)
|
|
cp = cp << 6 | (static_cast<unsigned char>(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<int64_t>(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<int, MAX_ITEMS> 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
|