From bb25353089248c3294c80f7734fea9ce1506e65c Mon Sep 17 00:00:00 2001
From: Max Gramser
Date: Fri, 18 Sep 2026 08:44:44 +0200
Subject: [PATCH] Forty-eight tiles, a tile over the whole page and tiles that
go to a page (0.2.74)
Max asked for many more tiles, a tile that takes the whole page as a light
switch you push without looking, and tiles that go to a page. The limit is
one tile per slot of the eight pages, 48, and the firmware keeps its tile
list on the heap sized to the layout (PSRAM on a Guition). A tile's size can
be `full`: one big button that lights up in the state colour while on, or the
double-width card's head with its small slider, direct controls or graph at
the bottom and the value the double-width card had no room for in between;
the clock, the forecast (with an hours strip) and the sun path fill the page.
Navigation tiles are the built-in entities screen.page_1 to screen.page_8:
an arrow or a chosen icon, the name and "Page n", a tap opens that page.
The app sends the three only to firmware 0.2.62+, the editor (the Vue port of
this round by the editor session, 00c9a4e) offers Normal / Double-width /
Full page and "Goes to page", and Claude in Home Assistant gets size: full and
the page tiles. Built-in tiles now send a chosen icon as its codepoint.
Found and fixed by the new host harness before release: the one-second tick
moved the analog clock's second hand (part 18) on a full forecast's hour label
during a page switch and corrupted it. Firmware 0.2.62; both C++ and Python
suites, host harness 37/37 on both boards, hardware pass on both bench boards
in docs/TEST_RESULTS_0274.md.
Co-Authored-By: Claude Fable 5.1
---
README.md | 10 +-
README_EXTENDED.md | 21 +-
components/smart_display/runtime_model.h | 71 +++++--
components/smart_display/runtime_tiles.h | 253 ++++++++++++++++++++---
docs/RELEASING.md | 26 +++
docs/TEST_RESULTS_0274.md | 111 ++++++++++
docs/images/cyd-full-climate.png | Bin 0 -> 11687 bytes
docs/images/cyd-full-light.png | Bin 0 -> 9437 bytes
docs/images/cyd-full-menu.png | Bin 0 -> 15859 bytes
docs/images/guition-full-climate.png | Bin 0 -> 21189 bytes
docs/images/guition-full-fan.png | Bin 0 -> 16458 bytes
docs/images/guition-full-light.png | Bin 0 -> 17032 bytes
docs/images/guition-full-menu.png | Bin 0 -> 27115 bytes
guition-4848s040.yaml | 41 +++-
home-like-2432s028.yaml | 41 +++-
packages/cyd.yaml | 41 +++-
packages/guition.yaml | 41 +++-
screen_manager/CHANGELOG.md | 10 +
screen_manager/README.md | 2 +-
screen_manager/app/claude_skill.py | 14 +-
screen_manager/app/core.py | 156 ++++++++++----
screen_manager/app/tile_icons.py | 10 +-
screen_manager/config.yaml | 2 +-
tests/test_full_page_tiles.py | 154 ++++++++++++++
tests/test_portal.py | 2 +-
tests/test_runtime_model.cpp | 30 ++-
tests/test_tile_events.py | 4 +-
tests/test_tile_icons.py | 18 +-
tools/generate_icons.py | 18 +-
29 files changed, 948 insertions(+), 128 deletions(-)
create mode 100644 docs/TEST_RESULTS_0274.md
create mode 100644 docs/images/cyd-full-climate.png
create mode 100644 docs/images/cyd-full-light.png
create mode 100644 docs/images/cyd-full-menu.png
create mode 100644 docs/images/guition-full-climate.png
create mode 100644 docs/images/guition-full-fan.png
create mode 100644 docs/images/guition-full-light.png
create mode 100644 docs/images/guition-full-menu.png
create mode 100644 tests/test_full_page_tiles.py
diff --git a/README.md b/README.md
index ebee90c..665932c 100644
--- a/README.md
+++ b/README.md
@@ -12,8 +12,9 @@ panel costs a fraction of that, sits on a table or in a wall box, and is always
lights, the heating or the vacuum.
**What.** Firmware for two affordable panels, the 2.8-inch CYD and the 4-inch Guition, with up to
-twenty tiles over four pages: lights, climate, blinds and curtains, the vacuum, media, the weather,
-history graphs, clocks and timers. An automation can put an alert on every screen when someone rings
+48 tiles over eight pages: lights, climate, blinds and curtains, the vacuum, media, the weather,
+history graphs, clocks and timers. A tile can take the whole page, one big switch you push without
+looking, and a tile can go to another page. An automation can put an alert on every screen when someone rings
the bell, and a Guition shows who is there with the doorbell camera's picture. A tap can run any
action Home Assistant has for a tile, and Dark mode suits a screen beside the bed.
@@ -31,6 +32,11 @@ also be changed on the screen or by an automation, and your own YAML for one scr
+
+
+
+
+
diff --git a/README_EXTENDED.md b/README_EXTENDED.md
index 7664803..0171263 100644
--- a/README_EXTENDED.md
+++ b/README_EXTENDED.md
@@ -15,8 +15,8 @@ on the screen itself, and how updates work.
## What you can configure
-- **Up to twenty tiles**, spread across up to four fixed pages of six
- tiles each. Search by entity, device, or room, and drag to reorder.
+- **Up to 48 tiles**, spread across up to eight fixed pages of six
+ tiles each (firmware 0.2.62+; twenty before). Search by entity, device, or room, and drag to reorder.
- **Per-tile settings:** a custom name, click behavior, a small slider where
supported, or a large value for things like temperature and power usage.
From firmware 0.2.13, the large value shows a small domain icon next to the
@@ -41,7 +41,22 @@ on the screen itself, and how updates work.
a **sun path** (`sun.sun`: horizon with the sun between sunrise and
sunset), a **timer** (`timer.*`, tapping starts or pauses it), and
**presence** (`person.*`). Pick them in the picker like any other tile, or
- drag them straight into the screen mockup; **Double-width** is an option for every tile.
+ drag them straight into the screen mockup; **Double-width** is an option for every tile, and so is
+ **Full page** (firmware 0.2.62+): the tile takes the whole page and is one big button that lights up
+ in its state colour while on, so a screen by the door switches the light when you push anywhere on it.
+ Its small slider, direct controls or graph sit at the bottom of the page. A **Go to page** tile
+ (`screen.page_2` to `screen.page_8`) opens another page: a full-page light switch on page 1, a menu on page 2.
+
+
+
+
+
+
+
+
+
+
+
- **Direct control on double-width tiles** (firmware 0.2.19+), like the rows in
Home Assistant: temperature − / + or mode buttons (climate), a toggle (switch,
light, fan), start/stop/dock (vacuum), open/stop/close or a
diff --git a/components/smart_display/runtime_model.h b/components/smart_display/runtime_model.h
index f92e846..0f475b7 100644
--- a/components/smart_display/runtime_model.h
+++ b/components/smart_display/runtime_model.h
@@ -7,13 +7,24 @@
#include
#include
#include
+// The tiles of a screen live on the heap, as many as the layout has (firmware 0.2.62+): a screen with twelve
+// tiles pays for twelve. On a board with PSRAM ESPHome's allocator puts them there, so the internal RAM the
+// WiFi stack and LVGL need stays free; without PSRAM it falls back to the internal heap. The host tests
+// and the host render build have neither, so they take the standard allocator.
+#if __has_include("esphome/core/defines.h")
+#include "esphome/core/defines.h"
+#endif
+#ifdef USE_ESP32
+#include "esphome/core/helpers.h"
+#endif
namespace runtime_tiles {
-constexpr size_t MAX_TILES = 20;
+// Eight pages of six slots: a screen holds at most one tile per slot (firmware 0.2.62+; twenty before).
constexpr size_t SLOTS_PER_PAGE = 6;
// Explicit grid positions (0.2.26+) address at most eight pages of six slots.
constexpr size_t MAX_PAGES = 8;
constexpr size_t MAX_SLOTS = MAX_PAGES * SLOTS_PER_PAGE;
+constexpr size_t MAX_TILES = MAX_SLOTS;
inline bool valid_entity(const std::string &entity) {
if (entity.size() > 120) return false;
auto dot = entity.find('.');
@@ -22,8 +33,12 @@ inline bool valid_entity(const std::string &entity) {
if (i != dot && !(entity[i] >= 'a' && entity[i] <= 'z') &&
!(entity[i] >= '0' && entity[i] <= '9') && entity[i] != '_') return false;
std::string domain = entity.substr(0, dot);
- // screen.* are built-in cards without a Home Assistant entity behind them.
- if (domain == "screen") return entity == "screen.clock" || entity == "screen.settings";
+ // screen.* are built-in cards without a Home Assistant entity behind them; screen.page_ (firmware 0.2.62+)
+ // only goes to page n, one such tile per page, so an entity still appears once on a screen.
+ if (domain == "screen") {
+ if (entity == "screen.clock" || entity == "screen.settings") return true;
+ return entity.size() == 13 && entity.compare(0, 12, "screen.page_") == 0 && entity[12] >= '1' && entity[12] <= static_cast('0' + MAX_PAGES);
+ }
for (const auto *allowed : {"light", "switch", "input_boolean", "scene", "script", "climate", "vacuum", "fan", "cover", "sensor", "binary_sensor", "input_select", "select", "number", "input_number", "weather", "media_player", "button", "input_button", "sun", "timer", "person", "camera", "image"})
if (domain == allowed) return true;
return false;
@@ -123,7 +138,8 @@ struct Tile {
bool has_hs_color = false;
int saturation = 0;
std::string tap = "auto", display = "standard", inline_control = "none";
- bool wide = false;
+ // Double width takes a row; full (firmware 0.2.62+) takes the whole page, all six slots, and is also wide.
+ bool wide = false, full = false;
// Direct control set on a wide card (firmware 0.2.19+); empty keeps the plain card.
std::string controls, device_class;
bool muted = false;
@@ -237,6 +253,11 @@ struct Tile {
// Two built-in cards, and only one of them is a clock that has to be redrawn every minute.
bool is_clock() const { return entity == "screen.clock"; }
bool is_settings() const { return entity == "screen.settings"; }
+ // A navigation tile (screen.page_, firmware 0.2.62+) and the page it goes to, counted from one.
+ bool is_page() const { return entity.size() == 13 && entity.compare(0, 12, "screen.page_") == 0; }
+ int page_target() const { return is_page() ? entity[12] - '0' : 0; }
+ // Slots a tile takes: one, a row of two, or the six of a page.
+ unsigned cells() const { return full ? SLOTS_PER_PAGE : wide ? 2u : 1u; }
// A scene, button or input button that never ran is "unknown" in Home Assistant, which still lets you press it
// (hui-button-entity-row disables only an unavailable one): its state is the moment it last ran (firmware 0.2.58+).
bool available() const {
@@ -263,14 +284,32 @@ struct Tile {
};
// Slot position of a tile within the fixed two-column, three-row pages.
struct Placement { uint8_t page = 0, slot = 0; };
-// Wide tiles start in the left column and take the whole row; a right-column
-// gap before them stays empty. Returns the page count (at least one).
-inline unsigned pack(const std::array &tiles, size_t count, std::array &out) {
+#ifdef USE_ESP32
+// ESPHome's allocator prefers PSRAM and falls back to the internal heap; this wrapper only gives it the
+// comparison std::vector wants.
+template struct TileAllocator {
+ using value_type = T;
+ TileAllocator() = default;
+ template TileAllocator(const TileAllocator &) {}
+ T *allocate(size_t n) { return esphome::RAMAllocator().allocate(n); }
+ void deallocate(T *p, size_t n) { esphome::RAMAllocator().deallocate(p, n); }
+ bool operator==(const TileAllocator &) const { return true; }
+ bool operator!=(const TileAllocator &) const { return false; }
+};
+using TileList = std::vector>;
+#else
+using TileList = std::vector;
+#endif
+// Wide tiles start in the left column and take the whole row; a right-column gap before them stays
+// empty. A full tile starts a page of its own; the slots it leaves behind stay empty. Returns the page
+// count (at least one).
+inline unsigned pack(const TileList &tiles, size_t count, std::array &out) {
unsigned position = 0;
for (size_t i = 0; i < count && i < MAX_TILES; ++i) {
- if (tiles[i].wide && position % 2 == 1) ++position;
+ if (tiles[i].full && position % SLOTS_PER_PAGE) position += SLOTS_PER_PAGE - position % SLOTS_PER_PAGE;
+ else if (tiles[i].wide && position % 2 == 1) ++position;
out[i] = {static_cast(position / SLOTS_PER_PAGE), static_cast(position % SLOTS_PER_PAGE)};
- position += tiles[i].wide ? 2 : 1;
+ position += tiles[i].cells();
}
unsigned pages = (position + SLOTS_PER_PAGE - 1) / SLOTS_PER_PAGE;
return pages ? pages : 1;
@@ -281,7 +320,7 @@ inline unsigned pack(const std::array &tiles, size_t count, std
struct Model;
inline unsigned place(const Model &m, std::array &out);
struct Model {
- std::array tiles;
+ TileList tiles;
// Absolute grid slot per tile when the manager sent `slots` (0.2.26+): gaps stay
// empty and a tile keeps its place. Without them the tiles pack in order.
std::array slots{};
@@ -312,13 +351,16 @@ struct Model {
}
}
changed = !configured || count != entities.size();
- for (size_t i = 0; i < entities.size(); ++i) if (tiles[i].entity != entities[i]) changed = true;
+ for (size_t i = 0; i < entities.size() && i < tiles.size(); ++i) if (tiles[i].entity != entities[i]) changed = true;
moved = !changed && (explicit_slots != !positions.empty());
for (size_t i = 0; i < positions.size() && !changed; ++i) if (slots[i] != positions[i]) moved = true;
// A title-only update must not interrupt an open control card.
title = name.empty() ? "Home" : name;
if (changed) {
- for (auto &tile : tiles) tile = Tile{};
+ // Freed before the new list is made, so the heap never holds both.
+ tiles.clear();
+ tiles.shrink_to_fit();
+ tiles.resize(entities.size());
count = entities.size();
for (size_t i = 0; i < count; ++i) tiles[i].entity = entities[i];
}
@@ -342,9 +384,10 @@ inline unsigned place(const Model &m, std::array &out) {
unsigned last = 0;
for (size_t i = 0; i < m.count && i < MAX_TILES; ++i) {
unsigned slot = m.slots[i];
- if (m.tiles[i].wide) slot &= ~1u;
+ if (m.tiles[i].full) slot -= slot % SLOTS_PER_PAGE;
+ else if (m.tiles[i].wide) slot &= ~1u;
out[i] = {static_cast(slot / SLOTS_PER_PAGE), static_cast(slot % SLOTS_PER_PAGE)};
- last = std::max(last, slot + (m.tiles[i].wide ? 2u : 1u));
+ last = std::max(last, slot + m.tiles[i].cells());
}
unsigned pages = (last + SLOTS_PER_PAGE - 1) / SLOTS_PER_PAGE;
return std::max({pages, 1u, std::min(m.pages, MAX_PAGES)});
diff --git a/components/smart_display/runtime_tiles.h b/components/smart_display/runtime_tiles.h
index 5b42bf7..221d464 100644
--- a/components/smart_display/runtime_tiles.h
+++ b/components/smart_display/runtime_tiles.h
@@ -39,6 +39,11 @@ inline Model model;
inline std::string inbox;
inline const lv_font_t *watch_font = nullptr;
inline const lv_font_t *mini_icon_font = nullptr;
+// The large icon font of a full-page card (firmware 0.2.62+): the domain icons only, so the flash stays free.
+inline const lv_font_t *big_icon_font = nullptr;
+// Page shown by the navigation bar and the swipe (the profile's tile_page), remembered by show_page.
+inline int *shown_page = nullptr;
+inline void go_to_page(int page);
inline const lv_font_t *watch_value_font = nullptr, *watch_icon_font = nullptr;
// Big digits for the clock card; the board profile sets it with the local time source.
inline const lv_font_t *clock_font = nullptr;
@@ -172,14 +177,17 @@ struct Widgets {
lv_obj_t *tile{}, *title{}, *value{}, *circle{}, *icon{}; size_t index{}; int cached_active = -1; lv_obj_t *slider{}, *progress{}, *unit{};
int title_x=0,title_y=0,value_x=0,value_y=0; const lv_font_t *value_font{}, *icon_font{};
// Wide cards span both columns; custom cards (clock, forecast, graph) draw into `extra`.
- bool wide=false; int base_width=0; lv_obj_t *extra{}; std::string extra_mode; std::array parts{}; lv_point_precise_t *points{};
+ // Full (firmware 0.2.62+) takes the whole page: the double-width card's head on top, a control at the bottom.
+ bool wide=false, full=false; int base_width=0, base_height=0; const lv_font_t *title_font{};
+ // `extra_full`: the size the parts were built for; a slot that changes between full and double width rebuilds them.
+ lv_obj_t *extra{}; std::string extra_mode; bool extra_full=false; std::array parts{}; lv_point_precise_t *points{};
// Analog clock: centre and radius of the dial, so the second hand can move without a card redraw.
int hand_cx=0, hand_cy=0, hand_r=0, hand_width=1;
// Soft area under a polyline (graph, sun path), painted by the extra container's draw event.
const lv_point_precise_t *fill_points{}; unsigned fill_count=0; int fill_x=0, fill_y=0, fill_base=0; lv_color_t fill_color{}; lv_opa_t fill_opa=0;
// Direct controls on a wide card: a panel at the right with pill keys, a -/+ pill,
// a slider or a toggle. Objects are rebuilt only when the control set changes.
- lv_obj_t *panel{}; std::string panel_mode; bool panel_dirty=false; int panel_w=0;
+ lv_obj_t *panel{}; std::string panel_mode; bool panel_dirty=false, panel_full=false; int panel_w=0;
std::array keys{}, key_icons{}; std::array key_commands{}; std::array key_args; std::array key_checked{};
lv_obj_t *pill{}, *pill_value{}, *knob{}, *control_slider{}; int knob_on=-1;
lv_color_t panel_accent{}, panel_text{};
@@ -195,6 +203,13 @@ 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;
}
+// Whether `font` draws the glyph a label's UTF-8 text starts with (a four-byte Material Design icon).
+inline bool font_has(const lv_font_t *font, const std::string &utf8) {
+ if (!font || utf8.size() < 4) return false;
+ const auto *b = reinterpret_cast(utf8.data());
+ uint32_t cp = (uint32_t(b[0] & 7) << 18) | (uint32_t(b[1] & 0x3F) << 12) | (uint32_t(b[2] & 0x3F) << 6) | (b[3] & 0x3F);
+ lv_font_glyph_dsc_t dsc; return lv_font_get_glyph_dsc(font, &dsc, cp, 0);
+}
// Home Assistant's API link as ESPHome itself tracks it: gone the moment the socket drops,
// back the moment HA reconnects, no guessing from message age.
inline bool ha_connected() { return esphome::api_is_connected(); }
@@ -478,9 +493,11 @@ inline std::string receive(const std::string &payload) {
// Direct controls (0.2.19+): the manager sends only the set a wide card really shows.
tile.controls = string(options["controls"], 16);
// Width arrives with the state, after the layout: re-pack the pages when it changes.
- bool was_wide = tile.wide;
- tile.wide = string(options["size"]) == "wide";
- bool repack = was_wide != tile.wide;
+ bool was_wide = tile.wide, was_full = tile.full;
+ std::string size = string(options["size"]);
+ tile.full = size == "full";
+ tile.wide = tile.full || size == "wide";
+ bool repack = was_wide != tile.wide || was_full != tile.full;
// Pre-computed extras: the manager converts time zones and fetches forecasts. What only some tiles
// carry is collected in `next` and replaces the tile's Extra at the end (see Tile::set_extra).
auto extra = root["x"];
@@ -1829,7 +1846,7 @@ inline const char *icon_for(const Tile &tile) {
if (d == "sun") return tile.state == "above_horizon" ? "\U000F059B" : "\U000F059C";
if (d == "timer") return "\U000F051B";
if (d == "person") return "\U000F0004";
- if (d == "screen") return tile.is_settings() ? "\U000F0493" : "\U000F0150";
+ if (d == "screen") return tile.is_settings() ? "\U000F0493" : tile.is_page() ? "\U000F0054" : "\U000F0150";
if (d == "camera" || d == "image") return "\U000F07AE";
return "\U000F0425";
}
@@ -1886,6 +1903,12 @@ inline void event(lv_event_t *event) {
// the link down too, as its card says (firmware 0.2.49+), and the clock card does nothing under a finger.
if (model.tiles[w.index].builtin()) {
auto &card = model.tiles[w.index];
+ if (card.is_page()) {
+ // A navigation tile goes to its page on a short tap, with the link down too; holding does nothing.
+ if (code != LV_EVENT_SHORT_CLICKED || card.tap == "none" || card.page_target() < 1) return;
+ if (allowed(esphome::millis(), 100 + w.index, card.entity)) go_to_page(card.page_target() - 1);
+ return;
+ }
if (!card.is_settings() || card.tap == "none" || (settings_screen::may_open && !settings_screen::may_open())) return;
if (allowed(esphome::millis(), 100 + w.index, card.entity)) settings_screen::open();
return;
@@ -1973,11 +1996,15 @@ inline int tile_width(const Widgets &w) {
int32_t width = lv_obj_get_style_width(w.tile, LV_PART_MAIN);
return LV_COORD_IS_PX(width) ? width : lv_obj_get_width(w.tile);
}
+inline int tile_height(const Widgets &w) {
+ int32_t height = lv_obj_get_style_height(w.tile, LV_PART_MAIN);
+ return LV_COORD_IS_PX(height) ? height : lv_obj_get_height(w.tile);
+}
inline int content_width(const Widgets &w) {
return tile_width(w) - lv_obj_get_style_space_left(w.tile, LV_PART_MAIN) - lv_obj_get_style_space_right(w.tile, LV_PART_MAIN);
}
inline int content_height(const Widgets &w) {
- return lv_obj_get_height(w.tile) - lv_obj_get_style_space_top(w.tile, LV_PART_MAIN) - lv_obj_get_style_space_bottom(w.tile, LV_PART_MAIN);
+ return tile_height(w) - lv_obj_get_style_space_top(w.tile, LV_PART_MAIN) - lv_obj_get_style_space_bottom(w.tile, LV_PART_MAIN);
}
inline void bind(size_t index, lv_obj_t *tile, lv_obj_t *title, lv_obj_t *value, lv_obj_t *circle, lv_obj_t *icon) {
lv_obj_update_layout(tile);
@@ -1987,9 +2014,11 @@ inline void bind(size_t index, lv_obj_t *tile, lv_obj_t *title, lv_obj_t *value,
lv_obj_update_layout(tile);
widgets[index] = {tile, title, value, circle, icon, index};
auto &w=widgets[index]; w.title_x=lv_obj_get_x(title);w.title_y=lv_obj_get_y(title);w.value_x=lv_obj_get_x(value);w.value_y=lv_obj_get_y(value);w.value_font=lv_obj_get_style_text_font(value,LV_PART_MAIN);
- w.base_width=lv_obj_get_width(tile);
+ w.base_width=lv_obj_get_width(tile);w.base_height=lv_obj_get_height(tile);
+ w.title_font=lv_obj_get_style_text_font(title,LV_PART_MAIN);
w.icon_font=lv_obj_get_style_text_font(icon,LV_PART_MAIN);
w.unit=lv_label_create(tile);lv_obj_set_style_text_font(w.unit,w.value_font,0);lv_obj_remove_flag(w.unit,LV_OBJ_FLAG_CLICKABLE);lv_obj_add_flag(w.unit,LV_OBJ_FLAG_HIDDEN);
+ lv_label_set_long_mode(w.unit,LV_LABEL_LONG_DOT);
// Fixed one-line boxes prevent wrapped names from overlapping the state on both boards.
lv_obj_set_height(title,lv_font_get_line_height(lv_obj_get_style_text_font(title,LV_PART_MAIN)));
lv_obj_set_height(value,lv_font_get_line_height(w.value_font));
@@ -2082,7 +2111,7 @@ inline void begin_extra(Widgets &w,const char *mode,int width,int height) {
w.extra=lv_obj_create(w.tile);lv_obj_remove_style_all(w.extra);lv_obj_remove_flag(w.extra,LV_OBJ_FLAG_CLICKABLE);lv_obj_remove_flag(w.extra,LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_event_cb(w.extra,extra_draw,LV_EVENT_DRAW_MAIN,&w);
}
- if(w.extra_mode!=mode){end_extra(w);w.extra_mode=mode;w.points=new lv_point_precise_t[POINT_BUFFER];w.cached_active=-1;}
+ if(w.extra_mode!=mode || w.extra_full!=w.full){end_extra(w);w.extra_mode=mode;w.extra_full=w.full;w.points=new lv_point_precise_t[POINT_BUFFER];w.cached_active=-1;}
w.fill_points=nullptr;w.fill_count=0;
// Parts that were hidden kept the colours of the card they last showed.
if(lv_obj_has_flag(w.extra,LV_OBJ_FLAG_HIDDEN)){w.cached_active=-1;lv_obj_remove_flag(w.extra,LV_OBJ_FLAG_HIDDEN);}
@@ -2151,7 +2180,8 @@ inline void render_clock(Widgets &w,const Tile &t,bool large,int width,int heigh
return;
}
// The dial keeps the same size with or without a card behind it.
- int dial=std::min(height,width),cx=dial/2,cy=height/2,outer=dial/2-1,radius=dial/2-(large?4:2);
+ // A full-page card centres its dial; the digital time and date beside a wide dial stay off it.
+ int dial=std::min(height,width),cx=(w.full?(width-dial)/2:0)+dial/2,cy=height/2,outer=dial/2-1,radius=dial/2-(large?4:2);
for(int i=0;i<12;++i){
float a=i*3.14159265f/6;bool cardinal=i%3==0;
if(cardinal && large){
@@ -2177,6 +2207,11 @@ inline void render_clock(Widgets &w,const Tile &t,bool large,int width,int heigh
second_hand(w,now);
if(new_hand)lv_obj_move_to_index(w.parts[18],lv_obj_get_index(w.parts[14]));
std::string day=now.is_valid()?std::to_string(now.day_of_month):"--";
+ if(w.full){
+ part_label(w,15,big,0,0,1,LV_TEXT_ALIGN_CENTER,"");
+ part_label(w,16,small,0,0,1,LV_TEXT_ALIGN_CENTER,"");
+ return;
+ }
if(w.wide){
int x=dial+(large?16:8),y=std::max(0,(height-text_h)/2);
part_label(w,15,big,x,y,width-x,LV_TEXT_ALIGN_CENTER,time_text(now));
@@ -2203,19 +2238,41 @@ inline void render_clock(Widgets &w,const Tile &t,bool large,int width,int heigh
int month_y=day_y+(big->line_height-big->base_line)-(small->line_height-small->base_line);
part_label(w,17,small,sx+day_size.x+gap,std::max(0,month_y),std::max(1,int(x+room-(sx+day_size.x+gap))),LV_TEXT_ALIGN_LEFT,month);
}
+inline int block_min(int icon_h,int temp_h,int text_h){return std::max(icon_h,temp_h)+2+text_h;}
// Current conditions on the left, five day columns on the right (wide cards only).
inline void render_forecast(Widgets &w,const Tile &t,bool large,int width,int height) {
begin_extra(w,"forecast",width,height);
const lv_font_t *title_font=lv_obj_get_style_text_font(w.title,LV_PART_MAIN);
const lv_font_t *temp_font=watch_value_font?watch_value_font:w.value_font,*day_icon=mini_icon_font?mini_icon_font:w.icon_font;
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);
+ // A full-page card (firmware 0.2.62+) adds the next hours under the days: time, icon and temperature per column.
+ int day_h=lv_font_get_line_height(title_font),icon_col=lv_font_get_line_height(day_icon);
+ unsigned hours=w.full?std::min(t.extra().hours.size(),large?6:4):0;
+ int hours_h=hours?day_h+icon_col+text_h:0;
+ int top_h=hours?std::max(block_min(icon_h,temp_h,text_h),height-hours_h-(large?12:6)):height;
+ for(unsigned j=0;j<6;++j){
+ if(j80):panel_metrics(large);
+ const lv_font_t *icon_font=w.full?w.icon_font:mini_icon_font?mini_icon_font:w.icon_font;
const lv_font_t *text_font=control_font?control_font:lv_obj_get_style_text_font(w.title,LV_PART_MAIN);
auto d=t.domain();
if(!w.panel){w.panel=panel_obj(w.tile,false);}
- if(w.panel_mode!=mode){
- end_panel(w);w.panel_mode=mode;w.panel_dirty=true;
+ if(w.panel_mode!=mode || w.panel_full!=w.full){
+ end_panel(w);w.panel_mode=mode;w.panel_full=w.full;w.panel_dirty=true;
if(tile_controls::is_key_row(mode)){
for(unsigned n=0;n<3;++n){panel_key(w,n,w.panel,m,m.key_w,m.key_h,false);panel_icon(w,n,icon_font);}
}else if(mode=="setpoint"||mode=="stepper"){
@@ -2444,7 +2505,9 @@ inline int layout_panel(Widgets &w,const Tile &t,bool large,int content_w,int co
if(!panel_w){hide_panel(w);return 0;}
// A panel shown again kept the colours of the card it last served.
if(lv_obj_has_flag(w.panel,LV_OBJ_FLAG_HIDDEN)){lv_obj_remove_flag(w.panel,LV_OBJ_FLAG_HIDDEN);w.panel_dirty=true;}
- lv_obj_set_size(w.panel,panel_w,panel_h);lv_obj_set_pos(w.panel,content_w-panel_w,std::max(0,(content_h-panel_h)/2));
+ lv_obj_set_size(w.panel,panel_w,panel_h);
+ if(w.full)lv_obj_set_pos(w.panel,std::max(0,(content_w-panel_w)/2),std::max(0,content_h-panel_h));
+ else lv_obj_set_pos(w.panel,content_w-panel_w,std::max(0,(content_h-panel_h)/2));
w.panel_w=panel_w+m.text_gap;
return w.panel_w;
}
@@ -2522,11 +2585,92 @@ inline void set_busy(Widgets &w,bool busy,bool large){
// Cover the whole card, padding included, at the width the card asks for: a slot that just turned
// wide is still single width in LVGL's own coordinates. Unchanged sizes cost LVGL nothing.
lv_obj_set_pos(w.busy,-lv_obj_get_style_pad_left(w.tile,LV_PART_MAIN),-lv_obj_get_style_pad_top(w.tile,LV_PART_MAIN));
- lv_obj_set_size(w.busy,tile_width(w),lv_obj_get_height(w.tile));
+ lv_obj_set_size(w.busy,tile_width(w),tile_height(w));
lv_obj_remove_flag(w.busy,LV_OBJ_FLAG_HIDDEN);
// Controls and custom parts created after the sheet would otherwise paint over its right side.
if(lv_obj_get_index(w.busy)!=(int32_t)lv_obj_get_child_count(w.tile)-1)lv_obj_move_foreground(w.busy);
}
+// The card that takes a whole page (firmware 0.2.62+). Without a control it is one big button: the icon in a
+// large circle with the name and the state under it, and the whole card lights up in the state colour while
+// on (see the palette below), so a wall switch reads from across the room and a push anywhere works. With a
+// small slider, direct controls or a graph the double-width card's head stays on top and the control takes
+// a strip at the bottom, so a tap anywhere else still does what a tap on the tile does. The built-in cards
+// (clock, forecast, sun path) simply get the whole page.
+inline void render_full(Widgets &w,const Tile &t,bool custom,bool clock,bool sunpath,bool graph,bool mini,bool with_panel,bool large,
+ const std::string &value,const std::string &unit,int content_w,int content_h) {
+ const bool big=w.base_height>80; // the board: a Guition card is large, a CYD card small
+ lv_obj_add_flag(w.unit,LV_OBJ_FLAG_HIDDEN);lv_obj_add_flag(w.progress,LV_OBJ_FLAG_HIDDEN);
+ if(custom){
+ lv_obj_add_flag(w.slider,LV_OBJ_FLAG_HIDDEN);hide_panel(w);
+ // The forecast and the sun path keep their board's layout (the CYD's two-row days); a taller dial is fine.
+ if(clock)render_clock(w,t,large,content_w,content_h);
+ else if(sunpath)render_sunpath(w,t,big,content_w,content_h);
+ else render_forecast(w,t,big,content_w,content_h);
+ return;
+ }
+ int value_h=lv_font_get_line_height(lv_obj_get_style_text_font(w.value,LV_PART_MAIN));
+ int gap=big?12:6;
+ if(!mini && !with_panel && !graph){
+ hide_extra(w);hide_panel(w);lv_obj_add_flag(w.slider,LV_OBJ_FLAG_HIDDEN);
+ const lv_font_t *name_font=room_label?lv_obj_get_style_text_font(room_label,LV_PART_MAIN):w.title_font;
+ int name_h=lv_font_get_line_height(name_font),circle=big?128:64;
+ int block=circle+gap+name_h+2+value_h,top=std::max(0,(content_h-block)/2);
+ lv_obj_set_size(w.circle,circle,circle);lv_obj_set_pos(w.circle,std::max(0,(content_w-circle)/2),top);
+ // The big icon font carries the domain icons; another chosen icon keeps its usual size in the big circle.
+ const lv_font_t *icon_font=big_icon_font && font_has(big_icon_font,icon_for(t))?big_icon_font:w.icon_font;
+ if(lv_obj_get_style_text_font(w.icon,LV_PART_MAIN)!=icon_font){set_font(w.icon,icon_font);lv_obj_center(w.icon);}
+ set_font(w.title,name_font);set_text_align(w.title,LV_TEXT_ALIGN_CENTER);set_text_align(w.value,LV_TEXT_ALIGN_CENTER);
+ lv_obj_set_height(w.title,name_h);
+ lv_obj_set_pos(w.title,0,top+circle+gap);lv_obj_set_width(w.title,content_w);
+ lv_obj_set_pos(w.value,0,top+circle+gap+name_h+2);lv_obj_set_width(w.value,content_w);
+ if(!unit.empty())label(w.value,value+" "+unit);
+ return;
+ }
+ // The head as on the double-width card: the circle at the left, name and state beside it.
+ set_font(w.title,w.title_font);set_text_align(w.title,LV_TEXT_ALIGN_LEFT);set_text_align(w.value,LV_TEXT_ALIGN_LEFT);
+ int title_h=lv_font_get_line_height(w.title_font);
+ lv_obj_set_height(w.title,title_h);
+ int head_h=std::max(1,w.base_height-lv_obj_get_style_space_top(w.tile,LV_PART_MAIN)-lv_obj_get_style_space_bottom(w.tile,LV_PART_MAIN));
+ int circle=big?54:36,line_gap=big?2:1,text_y=std::max(0,(head_h-(title_h+line_gap+value_h))/2);
+ lv_obj_set_size(w.circle,circle,circle);
+ if(lv_obj_get_style_text_font(w.icon,LV_PART_MAIN)!=w.icon_font){set_font(w.icon,w.icon_font);lv_obj_center(w.icon);}
+ lv_obj_set_pos(w.circle,0,big?12:std::max(0,(head_h-circle)/2));
+ lv_obj_set_pos(w.title,w.title_x,big?w.title_y:text_y);
+ lv_obj_set_pos(w.value,w.value_x,big?w.value_y:text_y+title_h+line_gap);
+ lv_obj_set_width(w.title,std::max(1,content_w-w.title_x));lv_obj_set_width(w.value,std::max(1,content_w-w.value_x));
+ if(mini){
+ // The small slider becomes a strip a thumb finds at the bottom; the room above it is the button.
+ hide_extra(w);hide_panel(w);
+ int strip=big?64:40;
+ lv_obj_set_size(w.slider,content_w,strip);
+ slider_handle(w.slider,content_w,strip);
+ lv_obj_remove_flag(w.slider,LV_OBJ_FLAG_HIDDEN);
+ if(!lv_obj_has_state(w.slider,LV_STATE_PRESSED) && lv_slider_get_value(w.slider)!=slider_value(t))lv_slider_set_value(w.slider,slider_value(t),LV_ANIM_OFF);
+ return;
+ }
+ lv_obj_add_flag(w.slider,LV_OBJ_FLAG_HIDDEN);
+ if(graph){
+ hide_panel(w);
+ render_graph(w,t,large,0,head_h+gap,content_w,std::max(8,content_h-head_h-gap));
+ return;
+ }
+ // Direct controls at the bottom, centred; the room between shows what the double-width card had no room for.
+ hide_extra(w);
+ if(!layout_panel(w,t,large,content_w,content_h)){hide_panel(w);return;}
+ int panel_h=lv_obj_get_style_height(w.panel,LV_PART_MAIN);
+ int mid_top=head_h,mid_h=content_h-head_h-gap-panel_h;
+ // The large value font carries digits, the degree sign and the percent sign; the clock font only digits and a colon.
+ std::string middle;const lv_font_t *mid_font=watch_value_font?watch_value_font:w.value_font;
+ auto d=t.domain();
+ if(d=="climate" && std::isfinite(t.current)){char b[32];snprintf(b,sizeof(b),"%.1f°",t.current);middle=b;}
+ else if(d=="cover" && std::isfinite(t.position)){middle=std::to_string(static_cast(std::lround(t.position)))+" %";}
+ else if(d=="media_player" && !t.extra().media_title.empty()){middle=t.extra().media_title;mid_font=room_label?lv_obj_get_style_text_font(room_label,LV_PART_MAIN):w.title_font;}
+ int mid_font_h=lv_font_get_line_height(mid_font);
+ if(middle.empty() || mid_h(std::lround(std::clamp(t.brightness, 0.0f, 255.0f) * 100 / 255))) + " %";
@@ -2569,7 +2714,7 @@ inline void render_slot(size_t slot) {
if(with_panel){std::string status=tile_controls::status_text(t);if(!status.empty())value=status;}
label(w.value, value);
bool mini=t.inline_control=="slider" && !watch && t.available();
- bool large_tile=lv_obj_get_height(w.tile)>80;
+ bool large_tile=tile_height(w)>80;
lap(swipe_profile::TEXT);
// Cards that replace the name/status layout entirely.
bool clock=t.is_clock(), forecast=d=="weather" && t.display=="forecast" && w.wide && t.extra().forecast.size()>0 && fresh() && t.available();
@@ -2588,7 +2733,11 @@ inline void render_slot(size_t slot) {
set_busy(w,w.busy_drawn,large_tile);
lap(swipe_profile::BUSY);
for(auto *o:{w.title,w.value,w.circle,w.unit})set_hidden(o,custom);
- if(custom){
+ if(w.full){
+ lap(swipe_profile::GEOMETRY);
+ render_full(w,t,custom,clock,sunpath,graph,mini,with_panel,large_tile,value,unit,content_w,content_h);
+ lap(swipe_profile::CUSTOM);
+ }else if(custom){
lv_obj_add_flag(w.slider,LV_OBJ_FLAG_HIDDEN);hide_panel(w);
lap(swipe_profile::GEOMETRY);
if(clock)render_clock(w,t,large_tile,content_w,content_h);
@@ -2596,6 +2745,9 @@ inline void render_slot(size_t slot) {
else render_forecast(w,t,large_tile,content_w,content_h);
lap(swipe_profile::CUSTOM);
}else{
+ // A slot that just held a full card gets its own name font, one-line box and left-aligned text back.
+ set_font(w.title,w.title_font);set_text_align(w.title,LV_TEXT_ALIGN_LEFT);set_text_align(w.value,LV_TEXT_ALIGN_LEFT);
+ title_height=lv_font_get_line_height(w.title_font);lv_obj_set_height(w.title,title_height);
int line_gap=large_tile?2:1,text_height=title_height+line_gap+value_height;
int slider_height=large_tile?28:8;
// A single-width graph takes the slider strip; a wide graph takes the right half.
@@ -2619,6 +2771,10 @@ inline void render_slot(size_t slot) {
// Use the requested coordinates: LVGL getters still return the previous
// layout until its next pass when a slot changes from watch/slider to normal.
int text_room=content_w-chart_w-(graph_side?(large_tile?10:6):0)-panel_w;
+ // A navigation tile ends in a chevron, as a row in Home Assistant's settings does.
+ const lv_font_t *chevron_font=mini_icon_font?mini_icon_font:w.icon_font;
+ int chevron_w=t.is_page() && !watch?lv_font_get_line_height(chevron_font):0;
+ if(chevron_w)text_room-=chevron_w+(large_tile?8:4);
lv_obj_set_width(w.title,std::max(1,text_room-text_x));
lv_obj_set_width(w.value,std::max(1,text_room-(watch?0:(mini||graph_strip)?text_x:w.value_x)));
if(watch){
@@ -2628,6 +2784,7 @@ inline void render_slot(size_t slot) {
lv_obj_set_pos(w.circle,0,group_y+(header-circle_size)/2);
lv_obj_set_pos(w.title,circle_size+(large_tile?6:4),group_y+(header-title_height)/2);
lv_obj_set_width(w.title,text_room-circle_size-(large_tile?6:4));
+ set_font(w.unit,w.value_font);set_text_align(w.unit,LV_TEXT_ALIGN_LEFT);
label(w.unit,unit);
lv_point_t size;lv_text_get_size(&size,unit.c_str(),w.value_font,0,0,LV_COORD_MAX,LV_TEXT_FLAG_EXPAND);
int unit_width=unit.empty()?0:std::min((int)size.x,text_room-20);
@@ -2636,6 +2793,10 @@ inline void render_slot(size_t slot) {
lv_obj_set_pos(w.unit,text_room-unit_width,value_y+value_height-lv_font_get_line_height(w.value_font));
lv_obj_set_size(w.unit,unit_width,lv_font_get_line_height(w.value_font));
set_hidden(w.unit,!unit_width);
+ }else if(chevron_w){
+ set_font(w.unit,chevron_font);set_text_align(w.unit,LV_TEXT_ALIGN_RIGHT);label(w.unit,"\U000F0142");
+ lv_obj_set_pos(w.unit,content_w-chevron_w,std::max(0,(header_height-chevron_w)/2));lv_obj_set_size(w.unit,chevron_w,chevron_w);
+ lv_obj_remove_flag(w.unit,LV_OBJ_FLAG_HIDDEN);
}else lv_obj_add_flag(w.unit,LV_OBJ_FLAG_HIDDEN);
lv_obj_set_size(w.slider,content_w,slider_height);
slider_handle(w.slider,content_w,slider_height);
@@ -2669,18 +2830,20 @@ inline void render_slot(size_t slot) {
set_color(w.slider,LV_STYLE_BG_COLOR,fill_color,LV_PART_INDICATOR);
set_color(w.slider,LV_STYLE_BG_COLOR,lv_color_hex(theme::tint(fill,51)),LV_PART_MAIN);
slider_bar(w.slider,slider_bar_shown(t,slider_on));
- set_color(w.tile,LV_STYLE_BG_COLOR,lv_color_hex(theme::surface(t.background)));
+ // A full-page card lights up in its state colour while on (firmware 0.2.62+): an amber lamp, a purple scene.
+ bool lit=w.full && on && available && !t.transparent;
+ set_color(w.tile,LV_STYLE_BG_COLOR,lv_color_hex(lit?theme::tint(state_color,51):theme::surface(t.background)));
set_number(w.tile,LV_STYLE_BORDER_WIDTH,1);
// "Background: none" hides only the card; geometry and padding stay identical,
// and the pressed flash still shows because it lives on the PRESSED state.
set_number(w.tile,LV_STYLE_BG_OPA,t.transparent ? LV_OPA_TRANSP : LV_OPA_COVER);
set_number(w.tile,LV_STYLE_BORDER_OPA,t.transparent ? LV_OPA_TRANSP : LV_OPA_COVER);
- set_color(w.tile,LV_STYLE_BORDER_COLOR,lv_color_hex(theme::outline(t.background)));
+ set_color(w.tile,LV_STYLE_BORDER_COLOR,lv_color_hex(lit?theme::tint(state_color,110):theme::outline(t.background)));
set_color(w.circle,LV_STYLE_BG_COLOR,circle_color);
set_color(w.icon,LV_STYLE_TEXT_COLOR,icon_color);
auto title_color=theme::color(theme::INK);
auto value_color=theme::color(t.background ? theme::SLATE : theme::MUTED);
- set_color(w.unit,LV_STYLE_TEXT_COLOR,theme::color(theme::SLATE));
+ set_color(w.unit,LV_STYLE_TEXT_COLOR,theme::color(t.is_page()?theme::CHEVRON:theme::SLATE));
set_color(w.title,LV_STYLE_TEXT_COLOR,title_color);
set_color(w.value,LV_STYLE_TEXT_COLOR,value_color);
style_panel(w,t,color,title_color);
@@ -2942,7 +3105,22 @@ inline bool check_tile_geometry() {
fits=lv_obj_get_width(w.tile)==expected;
if(!fits)ESP_LOGE("ui_test","Wide width FAIL slot=%u width=%d expected=%d",(unsigned)w.index,lv_obj_get_width(w.tile),expected);
}
- if(!custom){
+ if(w.full && widgets[4].tile){
+ // A full card ends exactly where the third row ends.
+ int expected=lv_obj_get_y(widgets[4].tile)-lv_obj_get_y(widgets[0].tile)+w.base_height;
+ if(lv_obj_get_height(w.tile)!=expected){fits=false;ESP_LOGE("ui_test","Full height FAIL slot=%u height=%d expected=%d",(unsigned)w.index,lv_obj_get_height(w.tile),expected);}
+ }
+ if(!custom && w.full){
+ // Everything inside the card, the name above the state, a slider or the controls below them.
+ fits=fits && title.x1>=content.x1 && title.x2<=content.x2 && value.x1>=content.x1 && value.x2<=content.x2 &&
+ title.y1>=content.y1 && title.y2=content.x1 && circle.x2<=content.x2 && circle.y1>=content.y1 && circle.y2<=content.y2;
+ if(!lv_obj_has_flag(w.slider,LV_OBJ_FLAG_HIDDEN)){
+ lv_obj_get_coords(w.slider,&track);
+ fits=fits && value.y2=content.x1 && title.x2<=content.x2 &&
value.x1>=content.x1 && value.x2<=content.x2 && title.y2title.y2;
+ bool chevron=w.index=content.y1 && (chevron ? title.x2title.y2);
}
}
if(w.panel && !lv_obj_has_flag(w.panel,LV_OBJ_FLAG_HIDDEN)){
// Direct controls stay inside the card, right of the name and status, and inside their panel.
lv_area_t panel;lv_obj_get_coords(w.panel,&panel);
- bool inside=panel.x1>=content.x1 && panel.x2<=content.x2 && panel.y1>=content.y1 && panel.y2<=content.y2 && (custom || (title.x2=content.x1 && panel.x2<=content.x2 && panel.y1>=content.y1 && panel.y2<=content.y2 && (custom || (w.full ? value.y2=content.x1 && part.x2<=content.x2 && part.y1>=content.y1 && part.y2<=content.y2;
if(!inside)ESP_LOGE("ui_test","Part bounds slot=%u mode=%s part=%d,%d..%d,%d content=%d,%d..%d,%d",(unsigned)w.index,w.extra_mode.c_str(),part.x1,part.y1,part.x2,part.y2,content.x1,content.y1,content.x2,content.y2);
fits=fits && inside;
- if(w.extra_mode=="graph" && !custom)fits=fits && (w.wide?part.x1>value.x2:part.y1>value.y2);
+ if(w.extra_mode=="graph" && !custom)fits=fits && (w.wide && !w.full?part.x1>value.x2:part.y1>value.y2);
}
}
if(!fits)ESP_LOGE("ui_test","Tile geometry FAIL slot=%u mode=%s wide=%d title_y=%d..%d value_y=%d..%d content_y=%d..%d",(unsigned)w.index,w.extra_mode.c_str(),w.wide,title.y1,title.y2,value.y1,value.y2,content.y1,content.y2);
@@ -3031,11 +3211,13 @@ inline int place_page(int page) {
int pages=place(model,placement);
page=std::clamp(page,0,pages-1);
int wide_width=widgets[0].tile && widgets[1].tile ? lv_obj_get_x(widgets[1].tile)-lv_obj_get_x(widgets[0].tile)+widgets[0].base_width : 2*widgets[0].base_width;
- for(size_t slot=0;slot=model.count || lv_obj_has_flag(w.tile,LV_OBJ_FLAG_HIDDEN))continue;
const auto &t=model.tiles[w.index];
if((t.is_clock() && new_minute) || (t.domain()=="timer" && t.state=="active") || (t.domain()=="sun" && second%60==0))card(w.index);
- // The second hand moves on its own: only its line is redrawn, and it hides during standby.
- else if(w.parts[18] && w.points && t.is_clock() && t.display=="analog")second_hand(w,now);
+ // The second hand moves on its own: only its line is redrawn, and it hides during standby. Only while the
+ // slot's parts are a dial: right after a page switch the slot already names the clock while its parts still
+ // belong to the card drawn before (a forecast's hour labels take part 18 too), until the fill draws the dial.
+ else if(w.parts[18] && w.points && w.extra_mode=="analog" && t.is_clock() && t.display=="analog")second_hand(w,now);
}
}
if(redraw && refresh)refresh();
diff --git a/docs/RELEASING.md b/docs/RELEASING.md
index 622a632..a9dbe7f 100644
--- a/docs/RELEASING.md
+++ b/docs/RELEASING.md
@@ -189,6 +189,32 @@ icons sit off-center in the browser); run `generate_packages.py` afterward.
The `MDI_GLYPH_*` substitutions are retired: `TILEn_ICON` in manual
profiles must come from the set. No changed preferences or keys.
+### Compatibility 0.2.74 / firmware 0.2.62
+
+Forty-eight tiles per screen, a tile over the whole page (`size: full`) and navigation tiles (`screen.page_1` to
+`screen.page_8`). The app sends any of the three only to firmware 0.2.62+ (`FULL_PAGE_MIN_FIRMWARE`); an older screen
+gets "Install screen firmware 0.2.62 or newer first". Older apps keep working with this firmware: they never send
+`full` or a page tile, and the firmware still packs `wide` as before.
+
+- Firmware: `MAX_TILES` is `MAX_SLOTS` (48). `Model::tiles` is a `std::vector` sized to the layout (a `TileAllocator`
+ over ESPHome's `RAMAllocator`, so a Guition keeps the list in PSRAM; the CYD's falls back to its heap); nothing may
+ index `tiles` beyond `count`. A `Tile` has `full` next to `wide` (a full tile is also wide) and `cells()`; `pack()`
+ and `place()` give a full tile a page of its own (`slot - slot % 6`). `screen.page_` is a built-in entity
+ (`Tile::is_page()`, `page_target()`); a tap calls `go_to_page`, which drives the profile's `tile_page` through
+ `show_page`. `Widgets` carries `full`, `base_height`, `title_font`, `panel_full` and 36 parts (the forecast's hours).
+ `render_full()` draws the page-sized card: one big button (128/64 px circle, the domain icon from the new
+ `materialdesign_icons_big` font, 64 px Guition / 40 px CYD, `tile_icons.BIG_GLYPHS` only), or the wide card's head with
+ the small slider, the control panel (`panel_metrics_full`, bottom-centred) or the graph at the bottom; a full card
+ that is on takes `theme::tint(state, 51)` as its surface. `check_tile_geometry` knows the full layout.
+- Wire: a state message's `o.size` may be `full`. Built-in tiles now send their options through `screen_options`, so a
+ chosen icon travels as its codepoint (older firmware ignored the name it got before). No `page` option: the page a
+ navigation tile opens is its entity.
+- App: `core.MAX_TILES` 48 (`LEGACY_MAX_TILES` 20 for `min_firmware`), `tile_size()`/`footprint(slot, size)` replace the
+ boolean `is_wide` in placement, `BUILTIN` gains the eight page tiles, `layout_snapshot` reports `size` and
+ `to_page`, `TILE_SIZES` accepts "full screen"/"whole page". The editor's tile limit is 48 from firmware 0.2.62
+ (`tileLimit()`), the tile sheet has Size Normal / Double-width / Full page and, for a page tile, "Goes to page"
+ (which swaps the entity). Storage unchanged: `size` is one more value of an existing option.
+
### Compatibility 0.2.72 / firmware 0.2.61
Firmware only (both board profiles; `cyd_ui.h` gains a comment); the app raises `FIRMWARE_VERSION`. No protocol,
diff --git a/docs/TEST_RESULTS_0274.md b/docs/TEST_RESULTS_0274.md
new file mode 100644
index 0000000..9f9f2e5
--- /dev/null
+++ b/docs/TEST_RESULTS_0274.md
@@ -0,0 +1,111 @@
+# Test results app 0.2.74 / firmware 0.2.62 (2026-09-18)
+
+Max: "zou het lukken om nog veel meer tiles toe te staan?", then a full-page tile ("soms willen mensen op pagina 1 een
+tile die het hele scherm beslaat ... zodat als ze hem hebben hangen als lichtknop, wil je dat hij meteen werkt zonder te
+kijken") and navigation tiles, with the designs approved first (option B "oplichtend", navigation tile N1) and the
+instruction "test dit echt uitvoerig, het is zo belangrijk dat dit goed werkt op beide borden op alle manieren". See
+CHANGELOG 0.2.74 and the compatibility note in docs/RELEASING.md. Built on 0.2.73 (`1ed42dc`, the Vue editor).
+
+## What changed, in one breath
+
+- 48 tiles per screen (one per slot of the eight pages), the firmware's tile list on the heap and sized to the layout
+ (PSRAM on a Guition), so a screen with twelve tiles pays for twelve.
+- Tile size `full`: the whole page. One big button (the domain icon in a 128 / 64 px circle from a new icon font that
+ carries only the domain icons, the name and the state centred) that lights up in the state colour while on; with a
+ small slider, direct controls or a graph the double-width card's head stays on top and the control takes a strip
+ at the bottom, with the value the double-width card had no room for (room temperature, blind position, media
+ title) in between. Clock, forecast (with an hours strip) and sun path fill the page.
+- Navigation tiles `screen.page_1` … `screen.page_8`: arrow (or a chosen icon), name, "Page n" and a chevron; a tap
+ opens that page, holding does nothing. One per page it goes to, so an entity still appears once on a screen.
+- Editor: Size Normal / Double-width / Full page, a "Goes to page" sheet, 48 from firmware 0.2.62, a full tile moves
+ the other tiles of its page to the first free cells after it.
+
+## Automated
+
+- Python (`.venv-portal`): 396 tests OK, including the new `tests/test_full_page_tiles.py` (sizes and footprints,
+ in-order packing, free slots, validation of full and page tiles, the firmware they need, the layout sensor, the wire
+ form of built-in tiles, tile events for Claude in Home Assistant, the 48 limit).
+- C++ (`c++ -std=c++17 -Wall -Wextra -pedantic`): 17/17 PASS. `tests/test_runtime_model.cpp` now covers 48 tiles, a
+ list sized to the layout, `screen.page_` validation and page targets, and full tiles in `pack()` and `place()`
+ (a page of their own, snapping to the page start, the page count).
+- `tools/generate_icons.py --check` and `tools/generate_packages.py` green; the new fourth icon font carries the
+ 31-glyph subset of `tile_icons.BIG_GLYPHS` in both profiles.
+- ESP32 check builds of both bench profiles compile (the first attempt caught one `std::max(int, long)`).
+
+## Firmware on the Mac (host builds, driven over the ESPHome API with a virtual finger)
+
+`.esphome/readme-render/fullpage/fullpage_host.py guition|cyd --compile` (git-ignored tooling) builds the board's
+package as a host program, feeds it the full-page demo home (`demo_home_fullpage.py` on 8098, the real add-on with
+screens that report firmware 0.2.62) and drives it with `fp_probe` (geometry, colours, texts, slider and key
+positions, the page shown, `check_tile_geometry()`), `fp_finger` (a pointer indev, so taps, holds and drags run through
+LVGL and the touch guard) and `fp_close`.
+
+Results: Guition 37/37 checks, CYD 37/37 checks (the CYD layout has a media player where the Guition has a blind, so
+the harness follows each board's demo layout). What the checks cover:
+
+| Page | Checks |
+|---|---|
+| Full light, no control | page size (448 × 348 / 302 × 164), geometry, lit amber while on with "59 %", centred 128 / 64 px circle, a tap in the middle and one low in a corner send `light.toggle`, the card drops the lit colour at once, a hold opens the light card |
+| Menu | three navigation tiles with "Page n" and a chevron (one double width), a tap on the first and on the wide one opens their pages, a hold does nothing |
+| Full climate, mode keys | head on top, three keys bottom-centred, "20.5°" between, the third key sends `climate.set_hvac_mode cool`, a tap between head and keys opens the climate card |
+| Full graph | head on top, no slider or panel, "21.4 °C", geometry, a tap asks for the history card |
+| Full blind (Guition) / full media player (CYD) | three keys and "60 %" / "Blue in Green" in the middle, the first key sends `cover.open_cover` / the middle key `media_player.media_play_pause`, a tap between head and keys opens the card |
+| Full forecast | geometry with the hours strip, a tap opens the weather card, six forecast/clock page flips with the second-hand tick running |
+| Full clock | geometry, a tap does nothing |
+| Full fan, small slider | a 64 / 40 px strip at the bottom, a tap above it sends `fan.toggle`, a drag from 30 % to 70 % sends `fan.set_percentage` near 70 |
+| 48 tiles | eight pages of six with every name in place, Next seven times reaches page 8, the last tile still answers a tap, 42 singles plus a full tile with a slider on page 8, and the same slot back as a single tile with its own geometry |
+
+### What the harness found before the release
+
+- **A crash on a page switch from a full forecast to a full clock.** The forecast's hour labels are parts 18 and up,
+ and part 18 is the analog clock's second hand. Right after a page switch the slot already names the clock while its
+ parts still belong to the forecast, and the one-second tick called `part_line` on an hour label:
+ `lv_line_set_points` on a label overwrites the label's text pointer, and the next `lv_obj_clean` freed a pointer
+ into the point buffer ("pointer being freed was not allocated", the macOS crash report Max pasted). Fixed: the tick
+ only moves the second hand while the slot's parts are a dial (`w.extra_mode == "analog"`), and a slot rebuilds its
+ parts when it changes between full and double width (a CYD dial is large on a full card and small on a wide one,
+ which swaps labels for lines at the same indices). Six forecast/clock page flips are part of the harness now.
+- A full-page tile got the domain's default direct control (a toggle on a light) instead of being the big button;
+ `resolve_controls` now gives a full tile no control unless one was chosen, and the editor shows "None" as chosen.
+- The middle value of a full climate or cover card used the clock font, which has no '.' or '°' (it drew "20□5□");
+ it uses the large value font now.
+- On the CYD the full forecast took the Guition's three-row day columns and ran them together; a full forecast and
+ sun path keep their board's layout.
+
+## Hardware
+
+Both bench boards were OTA-flashed with firmware 0.2.62 from the Mac (`esphome upload easy--device.yaml
+--device `), the Guition first, and report 0.2.62 in Home Assistant. Heap right after boot (HA sensors): Guition
+89 KB free / 60 KB minimum / 45 KB largest block (0.2.59: 76 / 57 / 33), CYD 116 KB / 107 KB / 59 KB (0.2.59: 106 /
+96 / 51).
+
+- `hw_push.py [cyd]` pushes one full-page scene straight to a board's `screen_message` action and, on the
+ Guition, snapshots the LVGL render through `ui_snapshot`: the big button (lit amber, in Max's dark look a brown
+ amber), the menu of navigation tiles with chevrons, the climate card with the room temperature and the mode keys,
+ the graph, the blind with its keys and position, the analog clock filling the page, the fan with its slider strip;
+ all as on the host. The CYD has no snapshot action; it answered `device_info` after every scene.
+- `hw_push48.py guition|cyd [flat]` pushed 43 tiles (seven full pages of singles and a full tile with a slider on
+ page 8) and 48 single tiles; both boards accepted them.
+- `hw_transition.py guition forecast,clock,forecast,button,clock,graph,forecast,fan,climate,clock` re-uses one slot for
+ one card kind after another over one API connection; the board stayed up throughout.
+- The home add-on (0.2.69) restores the real layout within about half a minute after every push, so the snapshots are
+ taken right after a push.
+
+## Editor (the real add-on on the demo home in the browser pane)
+
+Checked twice: first in the previous editor (static/app.js, where the round was built), then in the Vue editor that
+0.2.73 brought and into which session -75 ported the round (branch editor-fullpage, `00c9a4e`, with Vitest tests).
+Previous editor: "12 / 48"; a full-page tile fills its page on the mockup; the tile sheet offers Normal / Double-width /
+Full page with the hint; choosing Full page for Sam on a page with four other tiles kept Sam there and moved the other
+four to the first free cells of page 8; ArrowDown moved the full tile a page down and swapped the full climate tile up;
+"Go to page 4" from the picker landed on the free cell of page 8; the navigation-tile sheet shows "Goes to page" 1 … 8,
+Size Normal / Double-width, the icon and the colours; Save & send stored `size: full` and the page tiles at their slots.
+Vue editor: "8 pages · 12 / 48 tiles", the full tile drawn over its page, "Page 3 ›" on the navigation tiles, eight
+"Go to page n" rows in the library, the tile drawer with Size Normal / Double-width / Full page (selected, with the
+hint) and direct control None with the "at the bottom" hint, the navigation tile's drawer with Goes to page 1 … 8,
+Size Normal / Double-width and the colours.
+
+## Not tested
+
+- A real finger on the glass with the new firmware (Max's visual check after the release, [[max-visual-check-after-flash]]).
+- The add-on release on Max's Home Assistant Yellow: the add-on there is 0.2.69 until he updates it.
diff --git a/docs/images/cyd-full-climate.png b/docs/images/cyd-full-climate.png
new file mode 100644
index 0000000000000000000000000000000000000000..9d87c814bf6806897103f2e08c63bceb2c993711
GIT binary patch
literal 11687
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
literal 0
HcmV?d00001
diff --git a/docs/images/cyd-full-light.png b/docs/images/cyd-full-light.png
new file mode 100644
index 0000000000000000000000000000000000000000..1c06211f1b7ac4e249b620e2d739591f6a57f144
GIT binary patch
literal 9437
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
literal 0
HcmV?d00001
diff --git a/docs/images/guition-full-climate.png b/docs/images/guition-full-climate.png
new file mode 100644
index 0000000000000000000000000000000000000000..3a960f9bf81dd52c50c13f5f1c2807b12dc9bed1
GIT binary patch
literal 21189
zcmdqJXIxWR*EbxoU5tehW(E;~nK6J$Q&3T9&Zyuq2*ao#(p0LnC@mzA1S=Mh@gN`q
zK}7_qia=;duz@i^1O$XYfK_hvtG-+J9SjCr1svG-Ck(c@e(vADmO*bMo!@7`V1Fh6`+hqT-NWfSof+(lHIaC6
znPx9+_Y6_bIjw13HL_;k(uDh}KR(O6xWS=1d*cNShkY+IZf7oesTS@4YmGr}i_6U1
z_%Uv=UflhDCq1V%OW<$n(r=!olg7!%gcA-%HJGBwk^(u7bp&-ZvZJ6P@Mx(*(xE^v
zb1ph|)AilIpoxT2>w~SPVqY0U&rjVoR}1zoDjHb|gPr)bVqj0{kM-YqK?M*0bX*+~
zv#973RB+c|#Ru{H+o9iov2>hT0fX&HT20uVv-I&tbr>v;u<`t`LF^(J?Dh(;tAxCn
zxxS1aJmddBfjVls^lLO>zFPGkOX!DZm0++f>eu?w*XBkU`hATheK+*#gz;^C8Px9Q
z@Sm41^zg&C9*(JOfoi80TGhABHETm|dc0&UG;-6cCC4Ol(~)dlnhIMCgZ)*V3Pa$b
zsT`m@tXDk&)irgoI|nU)9N8!iE7O1;1pKbMy7f@WOP9M)#XXBwLeJlwfPO}5l-|RR
zK@Wo3%k$mOe76nyYh&DhtoFy;KygaY_^==9p`W`jsQ