Change a screen's settings on the screen itself (0.2.52)

Holding the top bar for about a second and a half opens a settings page on the
screen, with a blue line filling along the top edge while you hold. A built-in
screen.settings tile and the esphome.<screen>_open_settings action open it too.

The page is one constexpr table of rows in settings_screen.h: Brightness,
Night, Screen (clock, back to page 1, swiping, rotation) and This screen (name,
address, firmware, Home Assistant, Restart). Toggles flip on a tap, numbers and
times step with -/+ (a held time walks whole hours), a choice cycles in a chip,
and a row that depends on a switch above it is greyed out. The LVGL objects exist
only while the page is open: +608 bytes of RAM and +10.7 KB of flash on the CYD.
A change is saved, applied and reported through the existing screen_setting
event, so ESP Screens shows the same value within a second.

New setting auto_home (default on, 2 minutes): an open card, the settings page
or a later page goes back to page 1 after that long without a touch, not only on
standby. It travels beside the frozen eleven-key settings block with its own
preference, so older firmware and older apps keep working.

docs/SETTINGS.md is the recipe for adding a setting; the Claude skill explains
the page. Firmware 0.2.44.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-16 10:05:53 +02:00
parent 93c789dc48
commit b83164d61a
28 changed files with 1645 additions and 27 deletions
+4 -1
View File
@@ -22,7 +22,7 @@ inline bool valid_entity(const std::string &entity) {
!(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";
if (domain == "screen") return entity == "screen.clock" || entity == "screen.settings";
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"})
if (domain == allowed) return true;
return false;
@@ -146,6 +146,9 @@ struct Tile {
void observe(uint32_t next) { revision=next; if (pending && revision!=pending_revision) confirmed=true; }
std::string domain() const { return entity.substr(0, entity.find('.')); }
bool builtin() const { return domain() == "screen"; }
// 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"; }
bool available() const { return builtin() || (received && state != "unknown" && state != "unavailable" && !state.empty()); }
bool active() const {
return state == "on" || state == "cleaning" || state == "active" || (domain() == "person" && state == "home") ||
+45 -10
View File
@@ -4,6 +4,7 @@
#include "tile_palette.h"
#include "tile_icon.h"
#include "screen_settings.h"
#include "settings_screen.h"
#include "esphome/core/preferences.h"
#include "cyd_ui.h"
#include "light_controls.h"
@@ -20,11 +21,16 @@
namespace runtime_tiles {
inline bool enabled = false;
inline bool swipe_pages = false;
inline uint32_t rotation = 0;
inline bool rotation_supported = false;
// Swiping, rotation and going back to page 1 live in settings_screen, next to the other settings the
// screen can change itself; these names stay as the way the rest of the firmware reaches them.
using settings_screen::swipe_pages;
using settings_screen::rotation;
using settings_screen::rotation_supported;
using settings_screen::auto_home;
using settings_screen::auto_home_seconds;
inline esphome::ESPPreferenceObject rotation_preference;
inline esphome::ESPPreferenceObject swipe_preference;
inline esphome::ESPPreferenceObject home_preference;
inline Model model;
inline std::string inbox;
inline const lv_font_t *watch_font = nullptr;
@@ -69,19 +75,36 @@ inline uint32_t keepalive_seconds = 120;
inline std::string layout_rev;
inline std::function<void()> layout_changed, refresh, dismiss, settings_changed;
inline esphome::ESPPreferenceObject settings_preference;
// The two extra values of 0.2.44+ in one record: going back to page 1 by itself, and after how long.
struct HomeTimeout { uint32_t enabled = 1, seconds = 120; };
inline void load_settings() {
settings_preference = esphome::global_preferences->make_preference<screen_settings::Settings>(0x53435231);
swipe_preference = esphome::global_preferences->make_preference<uint32_t>(0x53575031);
home_preference = esphome::global_preferences->make_preference<HomeTimeout>(0x484F4D31);
uint32_t swipe_saved=0;
if(swipe_preference.load(&swipe_saved))swipe_pages=swipe_saved==1;
HomeTimeout home;
if(home_preference.load(&home) && home.seconds>=30 && home.seconds<=3600){
auto_home=home.enabled?1:0;auto_home_seconds=(int32_t)home.seconds;
}
if(rotation_supported){
rotation_preference=esphome::global_preferences->make_preference<uint32_t>(0x524F5431);
uint32_t saved=0;
if(rotation_preference.load(&saved) && saved<=270 && saved%90==0)rotation=saved;
if(rotation_preference.load(&saved) && saved<=270 && saved%90==0)rotation=(int32_t)saved;
}
screen_settings::Settings saved;
if (settings_preference.load(&saved) && saved.valid()) screen_settings::current = saved;
}
// Everything the screen remembers, written in one go. ESPHome batches the flash writes, so a row of
// taps on -/+ costs one write, and a value that did not change costs nothing.
inline void persist_settings() {
if (screen_settings::current.valid()) settings_preference.save(&screen_settings::current);
uint32_t swipe = swipe_pages ? 1 : 0;
swipe_preference.save(&swipe);
HomeTimeout home{(uint32_t) (auto_home ? 1 : 0), (uint32_t) std::clamp<int32_t>(auto_home_seconds, 30, 3600)};
home_preference.save(&home);
if (rotation_supported) { uint32_t turned = (uint32_t) rotation; rotation_preference.save(&turned); }
}
inline bool parse_settings(JsonObject obj, screen_settings::Settings &s) {
// Require the complete known schema; validate before touching any runtime state.
if (obj.size() != 11) return false;
@@ -181,6 +204,9 @@ inline std::string receive(const std::string &payload) {
entities.push_back(entity.as<std::string>());
}
if(!root["swipe_pages"].isNull() && !root["swipe_pages"].is<bool>())return false;
if(!root["auto_home"].isNull() && !root["auto_home"].is<bool>())return false;
if(!root["auto_home_seconds"].isNull() && (!root["auto_home_seconds"].is<unsigned>() ||
root["auto_home_seconds"].as<unsigned>()<30 || root["auto_home_seconds"].as<unsigned>()>3600))return false;
if(!root["rotation"].isNull() && (!root["rotation"].is<unsigned>() ||
root["rotation"].as<unsigned>()>270 || root["rotation"].as<unsigned>()%90!=0))return false;
if(!root["keepalive"].isNull() && (!root["keepalive"].is<unsigned>() ||
@@ -202,6 +228,12 @@ inline std::string receive(const std::string &payload) {
if(root["swipe_pages"].is<bool>() && swipe_pages!=root["swipe_pages"].as<bool>()){
swipe_pages=root["swipe_pages"].as<bool>();uint32_t saved=swipe_pages?1:0;swipe_preference.save(&saved);
}
if((root["auto_home"].is<bool>() && auto_home!=root["auto_home"].as<bool>()) ||
(root["auto_home_seconds"].is<unsigned>() && auto_home_seconds!=(int32_t)root["auto_home_seconds"].as<unsigned>())){
if(root["auto_home"].is<bool>())auto_home=root["auto_home"].as<bool>();
if(root["auto_home_seconds"].is<unsigned>())auto_home_seconds=(int32_t)root["auto_home_seconds"].as<unsigned>();
HomeTimeout home{(uint32_t)(auto_home?1:0),(uint32_t)auto_home_seconds};home_preference.save(&home);
}
bool rotation_changed=false;
if(rotation_supported && root["rotation"].is<unsigned>() && rotation!=root["rotation"].as<unsigned>()){
rotation=root["rotation"].as<unsigned>();rotation_preference.save(&rotation);rotation_changed=true;
@@ -961,7 +993,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 "\U000F0150";
if (d == "screen") return tile.is_settings() ? "\U000F0493" : "\U000F0150";
return "\U000F0425";
}
// HA duration strings ("0:05:00") to seconds; 0 when unusable.
@@ -1023,7 +1055,8 @@ inline void event(lv_event_t *event) {
if(code==LV_EVENT_SHORT_CLICKED && tile.tap=="toggle")open=false;
if(d=="media_player" && tile.tap=="toggle" && code==LV_EVENT_SHORT_CLICKED){action("media_player.toggle",tile.entity);return;}
if(d=="climate" && tile.tap=="toggle" && code==LV_EVENT_SHORT_CLICKED){action("climate.toggle",tile.entity);return;}
if(tile.builtin())return;
// The clock card does nothing under a finger; the settings card opens the page.
if(tile.builtin()){if(tile.is_settings())settings_screen::open();return;}
// A short tap runs or pauses the timer; holding opens the card with a cancel button.
if(d=="timer" && !open){action(tile.state=="active"?"timer.pause":"timer.start",tile.entity);return;}
if(d=="sensor" || d=="binary_sensor" || d=="weather" || d=="number" || d=="input_number" || d=="select" || d=="input_select" || d=="media_player" || d=="vacuum" || d=="sun" || d=="person" || d=="timer") { tile.begin(esphome::millis(),true); active_index=w.index; show_detail(w.index); return; }
@@ -1653,7 +1686,9 @@ inline void render_slot(size_t slot) {
bool watch=t.display=="watch";
std::string unit=watch?t.unit:"";
std::string value = t.state;
if (!fresh() || !t.available()) value = "Unavailable";
// Nothing in Home Assistant stands behind the settings card, so it says the same with the link down.
if (t.is_settings()) value = "Tap to open";
else if (!fresh() || !t.available()) value = "Unavailable";
else if (d == "light" && t.state == "on" && std::isfinite(t.brightness)) value = std::to_string(static_cast<int>(std::lround(std::clamp(t.brightness, 0.0f, 255.0f) * 100 / 255))) + " %";
else if (d == "climate" && std::isfinite(t.target)) { char b[32]; snprintf(b, sizeof(b), "%.1f°", t.target); value = b; }
else if (d == "person") value = t.state=="home"?"Home":t.state=="not_home"?"Away":t.state;
@@ -1676,7 +1711,7 @@ inline void render_slot(size_t slot) {
bool large_tile=lv_obj_get_height(w.tile)>80;
lap(swipe_profile::TEXT);
// Cards that replace the name/status layout entirely.
bool clock=t.builtin(), forecast=d=="weather" && t.display=="forecast" && w.wide && t.extra().forecast.size()>0 && fresh() && t.available();
bool clock=t.is_clock(), forecast=d=="weather" && t.display=="forecast" && w.wide && t.extra().forecast.size()>0 && fresh() && t.available();
bool sunpath=d=="sun" && t.display=="sunpath" && w.wide && !t.extra().sunrise.empty() && !t.extra().sunset.empty() && fresh() && t.available();
bool graph=d=="sensor" && t.display=="graph" && t.has_history && !clock;
bool custom=clock||forecast||sunpath;
@@ -2306,9 +2341,9 @@ inline void tick() {
for(size_t slot=0;slot<SLOTS_PER_PAGE;++slot){
auto &w=widgets[slot];if(!w.tile || w.index>=model.count || lv_obj_has_flag(w.tile,LV_OBJ_FLAG_HIDDEN))continue;
const auto &t=model.tiles[w.index];
if((t.builtin() && new_minute) || (t.domain()=="timer" && t.state=="active") || (t.domain()=="sun" && second%60==0))card(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.builtin() && t.display=="analog")second_hand(w,now);
else if(w.parts[18] && w.points && t.is_clock() && t.display=="analog")second_hand(w,now);
}
}
if(redraw && refresh)refresh();
+733
View File
@@ -0,0 +1,733 @@
#pragma once
#include <algorithm>
#include <array>
#include <cstdint>
#include <iterator>
#include <cstdio>
#include <string>
#include "screen_settings.h"
// The settings page on the screen itself (firmware 0.2.44+). It changes the same values ESP Screen
// Manager writes, for the moments you stand in front of the panel instead of behind a browser.
//
// Everything the page shows is one table: `pages[]` of `Row`s. A row knows how to read and write its
// value and what kind of control it needs, so a new setting is one line in that table -- no drawing
// code, no layout. docs/SETTINGS.md walks through adding one end to end.
//
// The model (rows, stepping, the texts) is free of LVGL so tests/test_settings_screen.cpp covers it
// on a PC; the drawing below it is behind SETTINGS_SCREEN_TEST.
namespace settings_screen {
// Values that are not part of the persisted `screen_settings::Settings` block, because that format is
// frozen at version 1: each of these keeps its own preference, so a new option never rewrites the
// old ones. The manager sends them as their own keys in the layout message.
inline int32_t swipe_pages = 0, rotation = 0, auto_home = 1, auto_home_seconds = 120;
inline bool rotation_supported = false;
// ------------------------------------------------------------------ the table
enum class Kind : uint8_t { page, toggle, number, duration, moment, choice, info, action };
using Read = int32_t (*)();
using Write = void (*)(int32_t);
using Text = std::string (*)();
using Shown = bool (*)();
using Run = void (*)();
struct Row {
Kind kind = Kind::info;
const char *label = "";
const char *icon = ""; // page rows and actions; a Material Design Icons glyph
Read read = nullptr; // toggle, number, duration, moment, choice
Write write = nullptr;
int32_t low = 0, high = 0, step = 0; // number: fixed step; duration: seconds, step from the ladder
const char *unit = ""; // straight after a number ("%")
const char *const *options = nullptr; // choice
uint8_t option_count = 0;
Text text = nullptr; // info
Run run = nullptr; // action
Shown shown = nullptr; // absent rows: rotation on a board that cannot turn
Shown enabled = nullptr; // greyed out while the switch it depends on is off
uint8_t opens = 0; // page rows: the page they open
};
constexpr Row page_row(const char *label, const char *icon, uint8_t opens) {
Row r{}; r.kind = Kind::page; r.label = label; r.icon = icon; r.opens = opens; return r;
}
constexpr Row toggle(const char *label, Read read, Write write, Shown shown = nullptr) {
Row r{}; r.kind = Kind::toggle; r.label = label; r.read = read; r.write = write; r.shown = shown; return r;
}
constexpr Row number(const char *label, Read read, Write write, int32_t low, int32_t high, int32_t step,
const char *unit = "", Shown enabled = nullptr) {
Row r{}; r.kind = Kind::number; r.label = label; r.read = read; r.write = write;
r.low = low; r.high = high; r.step = step; r.unit = unit; r.enabled = enabled; return r;
}
constexpr Row duration(const char *label, Read read, Write write, int32_t low, int32_t high,
Shown enabled = nullptr) {
Row r{}; r.kind = Kind::duration; r.label = label; r.read = read; r.write = write;
r.low = low; r.high = high; r.enabled = enabled; return r;
}
constexpr Row moment(const char *label, Read read, Write write, Shown enabled = nullptr) {
Row r{}; r.kind = Kind::moment; r.label = label; r.read = read; r.write = write;
r.low = 0; r.high = 1439; r.step = 15; r.enabled = enabled; return r;
}
constexpr Row choice(const char *label, Read read, Write write, const char *const *options, uint8_t count,
Shown shown = nullptr) {
Row r{}; r.kind = Kind::choice; r.label = label; r.read = read; r.write = write;
r.options = options; r.option_count = count; r.shown = shown; return r;
}
constexpr Row info(const char *label, Text text) {
Row r{}; r.kind = Kind::info; r.label = label; r.text = text; return r;
}
constexpr Row action(const char *label, const char *icon, Run run) {
Row r{}; r.kind = Kind::action; r.label = label; r.icon = icon; r.run = run; return r;
}
// ------------------------------------------------------------------ values
// One place where a change is kept, put into effect and sent to the manager, so the add-on's page
// shows what the screen shows. The same path the Home Assistant number entities already take.
inline void (*store)() = nullptr; // save the preferences
inline void (*apply)() = nullptr; // backlight, rotation, night
inline void (*report)(const char *, int32_t) = nullptr; // esphome.screen_setting event
inline void changed(const char *key, int32_t value) {
if (store) store();
if (apply) apply();
if (report) report(key, value);
}
// How coarse a duration steps: seconds near the bottom, quarters of an hour at the top. Stepping down
// reads the ladder one second lower, so up and down always land on the same values again.
inline int32_t ladder_step(int32_t seconds) {
if (seconds < 300) return 30;
if (seconds < 900) return 60;
if (seconds < 3600) return 300;
if (seconds < 7200) return 900;
return 1800;
}
// The value one press of - or + gives. Moments wrap around midnight; everything else stops at its end.
// `held` is a finger that has been on the key for a while: a time then walks whole hours instead of
// quarters, so setting night from 22:00 to 07:00 is one hold and not thirty-six taps.
inline int32_t stepped(const Row &row, int32_t value, int direction, bool held = false) {
if (row.kind == Kind::moment) {
int32_t step = held ? 60 : row.step;
// From a hold, land on the whole hour first; after that every step is an hour.
int32_t next = held && value % 60 ? (value / 60) * 60 + (direction > 0 ? 60 : 0) : value + direction * step;
int32_t span = row.high + 1;
next %= span;
return next < 0 ? next + span : next;
}
int32_t step = row.kind == Kind::duration ? ladder_step(direction < 0 ? value - 1 : value) : row.step;
return std::clamp<int32_t>(value + direction * step, row.low, row.high);
}
inline bool at_end(const Row &row, int32_t value, int direction) {
if (row.kind == Kind::moment) return false;
return stepped(row, value, direction) == value;
}
inline std::string duration_text(int32_t seconds) {
char buffer[24];
if (seconds < 60) { snprintf(buffer, sizeof(buffer), "%d sec", (int) seconds); return buffer; }
if (seconds < 3600) { snprintf(buffer, sizeof(buffer), "%d min", (int) (seconds / 60)); return buffer; }
int hours = seconds / 3600, minutes = (seconds % 3600) / 60;
if (minutes) snprintf(buffer, sizeof(buffer), "%d h %02d", hours, minutes);
else snprintf(buffer, sizeof(buffer), "%d h", hours);
return buffer;
}
// Minutes since midnight as the clock on this screen shows them.
inline std::string moment_text(int32_t minutes, bool clock_24h) {
char buffer[16];
int hour = minutes / 60, minute = minutes % 60;
if (clock_24h) snprintf(buffer, sizeof(buffer), "%02d:%02d", hour, minute);
else snprintf(buffer, sizeof(buffer), "%d:%02d %s", hour % 12 ? hour % 12 : 12, minute, hour < 12 ? "AM" : "PM");
return buffer;
}
// What the right-hand side of a row says. A toggle draws a switch instead, its text is for the tests.
inline std::string value_text(const Row &row) {
switch (row.kind) {
case Kind::toggle: return row.read && row.read() ? "On" : "Off";
case Kind::number: {
char buffer[24];
snprintf(buffer, sizeof(buffer), "%d%s", row.read ? (int) row.read() : 0, row.unit);
return buffer;
}
case Kind::duration: return duration_text(row.read ? row.read() : 0);
case Kind::moment: return moment_text(row.read ? row.read() : 0, screen_settings::current.clock_24h != 0);
case Kind::choice: {
int32_t index = row.read ? row.read() : 0;
return index >= 0 && index < row.option_count ? row.options[index] : "";
}
case Kind::info: return row.text ? row.text() : "";
default: return "";
}
}
inline bool visible_row(const Row &row) { return !row.shown || row.shown(); }
inline bool live_row(const Row &row) { return !row.enabled || row.enabled(); }
struct Page {
const char *title;
const Row *rows;
uint8_t count;
};
// How many rows fit, and whether the page therefore needs its pager. Reserving the pager only when it
// is really needed keeps a group that just fits on one page.
inline uint8_t fitting_rows(int span, int row_height, int gap, int pager, uint8_t count, bool &paged) {
int loose = std::max(1, (span + gap) / (row_height + gap));
paged = count > loose;
if (!paged) return count;
return (uint8_t) std::max(1, (span - pager + gap) / (row_height + gap));
}
// ------------------------------------------------------------------ the pages
inline constexpr const char *clock_options[] = {"12 hour", "24 hour"};
inline constexpr const char *rotation_options[] = {"0°", "90°", "180°", "270°"};
// Brightness, and when the screen dims by itself.
// Every reader says `-> int32_t` out loud: on the ESP32 that is `long`, and a lambda that returns a
// plain `int` (a ternary, say) then does not convert to Read at all. The Mac's host build accepts it,
// the board does not.
inline constexpr Row light_rows[] = {
number("Brightness", []() -> int32_t { return screen_settings::current.brightness; },
[](int32_t value) {
auto &s = screen_settings::current;
s.brightness = std::clamp<int32_t>(value, 5, 100);
s.standby_brightness = std::min(s.standby_brightness, s.brightness);
s.night_brightness = std::min(s.night_brightness, s.brightness);
changed("brightness", s.brightness);
}, 5, 100, 5, "%"),
toggle("Auto standby", []() -> int32_t { return screen_settings::current.standby_enabled; },
[](int32_t value) {
screen_settings::current.standby_enabled = value ? 1 : 0;
changed("standby_enabled", screen_settings::current.standby_enabled);
}),
duration("Standby after", []() -> int32_t { return screen_settings::current.standby_seconds; },
[](int32_t value) {
screen_settings::current.standby_seconds = value;
changed("standby_seconds", value);
}, 60, 86400, [] { return screen_settings::current.standby_enabled != 0; }),
number("Standby brightness", []() -> int32_t { return screen_settings::current.standby_brightness; },
[](int32_t value) {
auto &s = screen_settings::current;
s.standby_brightness = std::clamp<int32_t>(value, 0, s.brightness);
changed("standby_brightness", s.standby_brightness);
}, 0, 100, 5, "%", [] { return screen_settings::current.standby_enabled != 0; }),
};
// Darker between two times, so a panel in a hallway does not light up the bedroom.
inline constexpr Row night_rows[] = {
toggle("Night mode", []() -> int32_t { return screen_settings::current.night_enabled; },
[](int32_t value) {
screen_settings::current.night_enabled = value ? 1 : 0;
changed("night_enabled", screen_settings::current.night_enabled);
}),
moment("Starts", []() -> int32_t { return screen_settings::current.night_start; },
[](int32_t value) { screen_settings::current.night_start = value; changed("night_start", value); },
[] { return screen_settings::current.night_enabled != 0; }),
moment("Ends", []() -> int32_t { return screen_settings::current.night_end; },
[](int32_t value) { screen_settings::current.night_end = value; changed("night_end", value); },
[] { return screen_settings::current.night_enabled != 0; }),
number("Night brightness", []() -> int32_t { return screen_settings::current.night_brightness; },
[](int32_t value) {
auto &s = screen_settings::current;
s.night_brightness = std::clamp<int32_t>(value, 0, s.brightness);
changed("night_brightness", s.night_brightness);
}, 0, 100, 5, "%", [] { return screen_settings::current.night_enabled != 0; }),
};
// How the screen behaves under your finger: the clock, going back to the first page, swiping, turning.
inline constexpr Row screen_rows[] = {
choice("Clock", []() -> int32_t { return screen_settings::current.clock_24h ? 1 : 0; },
[](int32_t value) {
screen_settings::current.clock_24h = value ? 1 : 0;
changed("clock_24h", screen_settings::current.clock_24h);
}, clock_options, 2),
toggle("Back to page 1", []() -> int32_t { return auto_home; },
[](int32_t value) { auto_home = value ? 1 : 0; changed("auto_home", auto_home); }),
duration("After", []() -> int32_t { return auto_home_seconds; },
[](int32_t value) { auto_home_seconds = value; changed("auto_home_seconds", value); }, 30, 3600,
[] { return auto_home != 0; }),
toggle("Also on standby", []() -> int32_t { return screen_settings::current.home_on_standby; },
[](int32_t value) {
screen_settings::current.home_on_standby = value ? 1 : 0;
changed("home_on_standby", screen_settings::current.home_on_standby);
}),
toggle("Swipe between pages", []() -> int32_t { return swipe_pages; },
[](int32_t value) { swipe_pages = value ? 1 : 0; changed("swipe_pages", swipe_pages); }),
choice("Rotation", []() -> int32_t { return rotation / 90; },
[](int32_t value) { rotation = std::clamp<int32_t>(value, 0, 3) * 90; changed("rotation", rotation); },
rotation_options, 4, [] { return rotation_supported; }),
};
// Read-only facts plus the one action: what you want when something is stuck.
inline std::string (*name_text)() = nullptr;
inline std::string (*address_text)() = nullptr;
inline std::string (*firmware_text)() = nullptr;
inline std::string (*link_text)() = nullptr;
inline void (*restart_device)() = nullptr;
inline constexpr Row about_rows[] = {
info("Screen", [] { return name_text ? name_text() : std::string(); }),
info("Address", [] { return address_text ? address_text() : std::string(); }),
info("Firmware", [] { return firmware_text ? firmware_text() : std::string(); }),
info("Home Assistant", [] { return link_text ? link_text() : std::string(); }),
action("Restart", "\U000F0709", [] { if (restart_device) restart_device(); }),
};
inline constexpr Row menu_rows[] = {
page_row("Brightness", "\U000F0599", 1),
page_row("Night", "\U000F0594", 2),
page_row("Screen", "\U000F0379", 3),
page_row("This screen", "\U000F02FD", 4),
};
inline constexpr Page pages[] = {
{"Settings", menu_rows, (uint8_t) std::size(menu_rows)},
{"Brightness", light_rows, (uint8_t) std::size(light_rows)},
{"Night", night_rows, (uint8_t) std::size(night_rows)},
{"Screen", screen_rows, (uint8_t) std::size(screen_rows)},
{"This screen", about_rows, (uint8_t) std::size(about_rows)},
};
constexpr uint8_t PAGE_COUNT = (uint8_t) std::size(pages);
} // namespace settings_screen
#ifndef SETTINGS_SCREEN_TEST
#include "lvgl.h"
#include <functional>
namespace settings_screen {
// Bound by the board profile on boot: the fonts of the board and what the page may do to it.
inline const lv_font_t *title_font = nullptr, *row_font = nullptr, *icon_font = nullptr;
inline std::function<bool()> may_open; // awake, no calibration, runtime tiles alive
inline std::function<int()> drift; // how far the finger travelled during this touch
inline std::function<void()> before_open; // close whatever card is open first
// Holding the top bar opens the page; a finger that travels further than this is swiping, not holding.
inline uint32_t hold_ms = 1500;
inline int hold_limit = 20;
// How far a tap on a row may drift, the same figure the board's own touch guard uses for tiles
// (a centimetre on the resistive CYD, no limit on the capacitive Guition). 0 is no limit.
inline int tap_limit = 0;
// The page while it is on screen; nothing of it exists when it is closed, which is how a 320x240
// board with 60 KB of free heap can afford a settings page at all.
inline lv_obj_t *root = nullptr, *hold_area = nullptr, *hold_bar = nullptr;
inline uint8_t current_page = 0, first_row = 0;
inline int confirm_row = -1; // the action row that asked "tap again"
inline lv_timer_t *confirm_timer = nullptr;
struct Drawn {
lv_obj_t *card = nullptr, *value = nullptr, *knob = nullptr, *minus = nullptr, *plus = nullptr, *label = nullptr;
uint8_t row = 0;
};
inline std::array<Drawn, 8> drawn{};
inline uint8_t drawn_count = 0;
inline bool visible() { return root != nullptr; }
struct Metrics {
bool large;
int width, height, pad, radius, inset, bar, bar_y, rows_y, row_h, gap, pager, bottom, pill_w, pill_h, switch_w, switch_h;
};
inline Metrics metrics() {
int width = lv_display_get_horizontal_resolution(lv_display_get_default());
int height = lv_display_get_vertical_resolution(lv_display_get_default());
bool large = width >= 480;
return Metrics{large, width, height,
large ? 20 : 10, large ? 18 : 10, large ? 18 : 10,
large ? 56 : 36, large ? 16 : 7,
large ? 92 : 50, large ? 52 : 32, large ? 8 : 5,
large ? 40 : 26, large ? 16 : 10,
large ? 44 : 30, large ? 40 : 26,
large ? 62 : 40, large ? 34 : 22};
}
inline lv_obj_t *plain(lv_obj_t *parent, int x, int y, int w, int h) {
auto *obj = lv_obj_create(parent);
lv_obj_remove_style_all(obj);
lv_obj_remove_flag(obj, static_cast<lv_obj_flag_t>(LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE));
lv_obj_set_pos(obj, x, y);
lv_obj_set_size(obj, w, h);
return obj;
}
inline lv_obj_t *text(lv_obj_t *parent, const std::string &value, const lv_font_t *font, uint32_t color,
lv_text_align_t align = LV_TEXT_ALIGN_LEFT) {
auto *label = lv_label_create(parent);
lv_label_set_text(label, value.c_str());
lv_label_set_long_mode(label, LV_LABEL_LONG_DOT);
lv_obj_remove_flag(label, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_text_font(label, font, 0);
lv_obj_set_style_text_color(label, lv_color_hex(color), 0);
lv_obj_set_style_text_align(label, align, 0);
lv_obj_set_height(label, lv_font_get_line_height(font));
return label;
}
inline void draw();
// ---- the controls on a row ----
// A round key like the -/+ on the climate card: the finger sees the press in the same frame.
inline lv_obj_t *key(lv_obj_t *parent, const char *glyph, int x, int y, int w, int h, lv_event_cb_t handler,
void *data) {
auto *button = plain(parent, x, y, w, h);
lv_obj_add_flag(button, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_bg_opa(button, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(button, lv_color_hex(0xE9EFF4), 0);
lv_obj_set_style_bg_color(button, lv_color_hex(0xC3D5E3), LV_STATE_PRESSED);
lv_obj_set_style_radius(button, LV_RADIUS_CIRCLE, 0);
lv_obj_set_style_opa(button, LV_OPA_40, LV_STATE_DISABLED);
auto *label = text(button, glyph, icon_font ? icon_font : row_font, 0x1B1B1B);
lv_obj_set_width(label, LV_SIZE_CONTENT);
lv_obj_center(label);
lv_obj_add_event_cb(button, handler, LV_EVENT_SHORT_CLICKED, data);
lv_obj_add_event_cb(button, handler, LV_EVENT_LONG_PRESSED_REPEAT, data);
return button;
}
// Not an lv_switch: the row itself is the target, so the pill only has to show the state and can
// never swallow half the taps that land next to it.
inline lv_obj_t *pill(lv_obj_t *parent, int x, int y, int w, int h, bool on) {
auto *track = plain(parent, x, y, w, h);
lv_obj_set_style_bg_opa(track, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(track, lv_color_hex(on ? 0x009FE3 : 0xC4C4C4), 0);
lv_obj_set_style_radius(track, LV_RADIUS_CIRCLE, 0);
int inset = std::max(2, h / 10), size = h - 2 * inset;
auto *knob = plain(track, on ? w - inset - size : inset, inset, size, size);
lv_obj_set_style_bg_opa(knob, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(knob, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_radius(knob, LV_RADIUS_CIRCLE, 0);
return knob;
}
inline void move_knob(Drawn &d, const Row &row) {
if (!d.knob) return;
auto *track = lv_obj_get_parent(d.knob);
bool on = row.read && row.read();
int w = lv_obj_get_width(track), size = lv_obj_get_width(d.knob), inset = (lv_obj_get_height(track) - size) / 2;
lv_obj_set_style_bg_color(track, lv_color_hex(on ? 0x009FE3 : 0xC4C4C4), 0);
lv_obj_set_x(d.knob, on ? w - inset - size : inset);
}
// After any change: every row on this page tells its own value again. Cheap (eight labels at most) and
// it keeps rows that lean on each other honest -- lowering Brightness also lowers the two dim levels.
inline void refresh() {
const Page &page = pages[current_page];
for (uint8_t i = 0; i < drawn_count; ++i) {
Drawn &d = drawn[i];
const Row &row = page.rows[d.row];
if (d.value) lv_label_set_text(d.value, value_text(row).c_str());
if (d.knob) move_knob(d, row);
// A row whose switch is off (night times, the standby levels) is there but does nothing, and says so.
bool live = live_row(row);
if (d.card) lv_obj_set_style_opa(d.card, live ? LV_OPA_COVER : LV_OPA_50, 0);
if (d.minus && row.read) {
bool off = !live || at_end(row, row.read(), -1);
if (off) lv_obj_add_state(d.minus, LV_STATE_DISABLED); else lv_obj_remove_state(d.minus, LV_STATE_DISABLED);
}
if (d.plus && row.read) {
bool off = !live || at_end(row, row.read(), 1);
if (off) lv_obj_add_state(d.plus, LV_STATE_DISABLED); else lv_obj_remove_state(d.plus, LV_STATE_DISABLED);
}
}
}
inline void forget_confirm() {
if (confirm_timer) { lv_timer_delete(confirm_timer); confirm_timer = nullptr; }
confirm_row = -1;
}
// ---- events ----
inline bool steady(int limit) { return !drift || limit <= 0 || drift() <= limit; }
// Holding - or + repeats, but at the pace of the climate card (five a second) instead of LVGL's ten.
// After a second of holding the steps grow; a single tap is always the small step.
inline uint32_t last_repeat = 0;
inline uint8_t repeats = 0;
constexpr uint8_t REPEATS_BEFORE_FAST = 5;
inline void step_event(lv_event_t *event) {
if (!steady(tap_limit)) return;
bool held = false;
if (lv_event_get_code(event) == LV_EVENT_LONG_PRESSED_REPEAT) {
uint32_t now = lv_tick_get();
if (now - last_repeat < 200) return;
last_repeat = now;
if (repeats < 255) ++repeats;
held = repeats > REPEATS_BEFORE_FAST;
} else {
repeats = 0;
}
int data = (int) (intptr_t) lv_event_get_user_data(event);
const Page &page = pages[current_page];
uint8_t index = (uint8_t) (data >> 1);
if (index >= page.count) return;
const Row &row = page.rows[index];
if (!row.read || !row.write || !live_row(row)) return;
int32_t value = row.read();
int32_t next = stepped(row, value, data & 1 ? 1 : -1, held);
if (next == value) return;
row.write(next);
refresh();
}
inline void row_event(lv_event_t *event) {
if (!steady(tap_limit)) return;
int index = (int) (intptr_t) lv_event_get_user_data(event);
const Page &page = pages[current_page];
if (index < 0 || index >= page.count) return;
const Row &row = page.rows[index];
if (!live_row(row)) return;
if (row.kind == Kind::page) { current_page = row.opens; first_row = 0; forget_confirm(); draw(); return; }
if (row.kind == Kind::toggle && row.read && row.write) { row.write(row.read() ? 0 : 1); refresh(); return; }
if (row.kind == Kind::choice && row.read && row.write && row.option_count) {
row.write((row.read() + 1) % row.option_count);
refresh();
return;
}
if (row.kind == Kind::action) {
// Nothing on this page is worth a dialog, except the one row that takes the screen away for ten
// seconds: it asks once, in place, and forgets the question after five.
if (confirm_row == index) { forget_confirm(); if (row.run) row.run(); return; }
forget_confirm();
confirm_row = index;
confirm_timer = lv_timer_create([](lv_timer_t *) { forget_confirm(); draw(); }, 5000, nullptr);
lv_timer_set_repeat_count(confirm_timer, 1);
draw();
}
}
inline void close();
inline void back_event(lv_event_t *) {
forget_confirm();
if (current_page == 0) { close(); return; }
current_page = 0;
first_row = 0;
draw();
}
inline void pager_event(lv_event_t *event) {
int direction = (int) (intptr_t) lv_event_get_user_data(event);
int next = (int) first_row + direction;
if (next < 0) return;
first_row = (uint8_t) next;
forget_confirm();
draw();
}
// ---- drawing ----
inline void draw() {
if (!root) return;
Metrics m = metrics();
lv_obj_clean(root);
drawn_count = 0;
const Page &page = pages[current_page];
// Rows a board does not have (rotation on the CYD) leave the table out of sight entirely.
std::array<uint8_t, 12> shown{};
uint8_t count = 0;
for (uint8_t i = 0; i < page.count && count < shown.size(); ++i)
if (visible_row(page.rows[i])) shown[count++] = i;
// Top bar: the same round back arrow and centred name as the cards of a tile.
auto *back = plain(root, m.pad, m.bar_y, m.bar, m.bar);
lv_obj_add_flag(back, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_bg_opa(back, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(back, lv_color_hex(0xEEEEEE), 0);
lv_obj_set_style_bg_color(back, lv_color_hex(0xDADADA), LV_STATE_PRESSED);
lv_obj_set_style_radius(back, LV_RADIUS_CIRCLE, 0);
auto *arrow = text(back, "\U000F004D", icon_font ? icon_font : row_font, 0x1B1B1B);
lv_obj_set_width(arrow, LV_SIZE_CONTENT);
lv_obj_center(arrow);
lv_obj_add_event_cb(back, back_event, LV_EVENT_SHORT_CLICKED, nullptr);
const lv_font_t *heading = title_font ? title_font : row_font;
auto *title = text(root, page.title, heading, 0x1B1B1B, LV_TEXT_ALIGN_CENTER);
lv_obj_set_width(title, m.width - 2 * (m.pad + m.bar + 8));
lv_obj_set_pos(title, m.pad + m.bar + 8, m.bar_y + (m.bar - lv_font_get_line_height(heading)) / 2);
int span = m.height - m.rows_y - m.bottom;
bool paged = false;
uint8_t per_page = std::min<uint8_t>(fitting_rows(span, m.row_h, m.gap, m.pager, count, paged), drawn.size());
// Always start a page on a page boundary, and never scroll a group that fits.
first_row = paged && first_row < count ? (uint8_t) ((first_row / per_page) * per_page) : 0;
int label_h = lv_font_get_line_height(row_font);
for (uint8_t slot = 0; slot < per_page && first_row + slot < count; ++slot) {
uint8_t index = shown[first_row + slot];
const Row &row = page.rows[index];
Drawn &d = drawn[slot];
d = Drawn{};
d.row = index;
bool tappable = row.kind == Kind::page || row.kind == Kind::toggle || row.kind == Kind::choice ||
row.kind == Kind::action;
bool asking = row.kind == Kind::action && confirm_row == index;
d.card = plain(root, m.pad, m.rows_y + slot * (m.row_h + m.gap), m.width - 2 * m.pad, m.row_h);
lv_obj_set_style_bg_opa(d.card, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(d.card, lv_color_hex(asking ? 0x009FE3 : 0xFFFFFF), 0);
lv_obj_set_style_radius(d.card, m.radius, 0);
lv_obj_set_style_border_width(d.card, 1, 0);
lv_obj_set_style_border_color(d.card, lv_color_hex(asking ? 0x009FE3 : 0xDDDDDD), 0);
if (tappable) {
lv_obj_add_flag(d.card, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_bg_color(d.card, lv_color_hex(asking ? 0x0083BC : 0xF1F1F1), LV_STATE_PRESSED);
lv_obj_add_event_cb(d.card, row_event, LV_EVENT_SHORT_CLICKED, (void *) (intptr_t) index);
}
int left = m.inset;
if (row.icon && *row.icon) {
auto *glyph = text(d.card, row.icon, icon_font ? icon_font : row_font, asking ? 0xFFFFFF : 0x4A4A4A);
int icon_h = lv_font_get_line_height(icon_font ? icon_font : row_font);
lv_obj_set_width(glyph, LV_SIZE_CONTENT);
lv_obj_set_pos(glyph, left, (m.row_h - icon_h) / 2);
left += icon_h + (m.large ? 12 : 8);
}
d.label = text(d.card, asking ? "Tap again to restart" : row.label, row_font, asking ? 0xFFFFFF : 0x1B1B1B);
lv_obj_set_pos(d.label, left, (m.row_h - label_h) / 2);
int right = m.width - 2 * m.pad - m.inset; // free space from the right edge of the card
if (row.kind == Kind::page) {
auto *chevron = text(d.card, "\U000F0142", icon_font ? icon_font : row_font, 0xB0B0B0);
int icon_h = lv_font_get_line_height(icon_font ? icon_font : row_font);
lv_obj_set_width(chevron, LV_SIZE_CONTENT);
lv_obj_set_pos(chevron, right - icon_h, (m.row_h - icon_h) / 2);
lv_obj_set_width(d.label, right - icon_h - left - 6);
} else if (row.kind == Kind::toggle) {
d.knob = pill(d.card, right - m.switch_w, (m.row_h - m.switch_h) / 2, m.switch_w, m.switch_h, row.read && row.read());
lv_obj_set_width(d.label, right - m.switch_w - left - 6);
} else if (row.kind == Kind::number || row.kind == Kind::duration || row.kind == Kind::moment) {
int y = (m.row_h - m.pill_h) / 2, value_w = m.large ? 116 : 74;
d.plus = key(d.card, "\U000F0415", right - m.pill_w, y, m.pill_w, m.pill_h, step_event,
(void *) (intptr_t) (index * 2 + 1));
d.minus = key(d.card, "\U000F0374", right - m.pill_w - value_w - m.pill_w, y, m.pill_w, m.pill_h, step_event,
(void *) (intptr_t) (index * 2));
d.value = text(d.card, value_text(row), row_font, 0x1B1B1B, LV_TEXT_ALIGN_CENTER);
lv_obj_set_width(d.value, value_w);
lv_obj_set_pos(d.value, right - m.pill_w - value_w, (m.row_h - label_h) / 2);
lv_obj_set_width(d.label, right - 2 * m.pill_w - value_w - left - 6);
} else if (row.kind == Kind::choice) {
int value_w = m.large ? 150 : 96, chip_h = m.pill_h;
auto *chip = plain(d.card, right - value_w, (m.row_h - chip_h) / 2, value_w, chip_h);
lv_obj_set_style_bg_opa(chip, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(chip, lv_color_hex(0xE9EFF4), 0);
lv_obj_set_style_radius(chip, LV_RADIUS_CIRCLE, 0);
d.value = text(chip, value_text(row), row_font, 0x1B1B1B, LV_TEXT_ALIGN_CENTER);
lv_obj_set_width(d.value, value_w - 8);
lv_obj_set_pos(d.value, 4, (chip_h - label_h) / 2);
lv_obj_set_width(d.label, right - value_w - left - 6);
} else if (row.kind == Kind::info) {
d.value = text(d.card, value_text(row), row_font, 0x616161, LV_TEXT_ALIGN_RIGHT);
int value_w = (m.width - 2 * m.pad) / 2;
lv_obj_set_width(d.value, value_w);
lv_obj_set_pos(d.value, right - value_w, (m.row_h - label_h) / 2);
lv_obj_set_width(d.label, right - value_w - left - 6);
} else {
lv_obj_set_width(d.label, right - left);
}
drawn_count = (uint8_t) (slot + 1);
}
refresh();
if (!paged) return;
// The same pager as the tile pages, in the same place, so it is the one that is already learned.
uint8_t page_number = (uint8_t) (first_row / per_page + 1);
uint8_t page_total = (uint8_t) ((count + per_page - 1) / per_page);
int y = m.height - m.pager - m.bottom / 2, half = (m.width - 2 * m.pad) / 2 - 20;
for (int side = 0; side < 2; ++side) {
bool enabled = side ? page_number < page_total : page_number > 1;
if (!enabled) continue;
auto *bar = plain(root, side ? m.width - m.pad - half : m.pad, y, half, m.pager);
lv_obj_add_flag(bar, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_bg_opa(bar, LV_OPA_TRANSP, 0);
lv_obj_set_style_bg_opa(bar, LV_OPA_COVER, LV_STATE_PRESSED);
lv_obj_set_style_bg_color(bar, lv_color_hex(0xDCDCDC), LV_STATE_PRESSED);
lv_obj_set_style_radius(bar, m.radius, 0);
auto *label = text(bar, side ? "Next >" : "< Previous", row_font, 0x1B1B1B,
side ? LV_TEXT_ALIGN_RIGHT : LV_TEXT_ALIGN_LEFT);
lv_obj_set_width(label, half - 12);
lv_obj_set_pos(label, 6, (m.pager - label_h) / 2);
lv_obj_add_event_cb(bar, pager_event, LV_EVENT_SHORT_CLICKED,
(void *) (intptr_t) (side ? per_page : -per_page));
}
auto *counter = text(root, std::to_string(page_number) + " / " + std::to_string(page_total), row_font, 0x616161,
LV_TEXT_ALIGN_CENTER);
lv_obj_set_width(counter, m.width);
lv_obj_set_pos(counter, 0, y + (m.pager - label_h) / 2);
}
inline void hide_hold_bar() {
if (!hold_bar) return;
lv_anim_delete(hold_bar, nullptr);
lv_obj_add_flag(hold_bar, LV_OBJ_FLAG_HIDDEN);
}
inline void open(uint8_t page = 0) {
hide_hold_bar();
current_page = page < PAGE_COUNT ? page : 0;
first_row = 0;
forget_confirm();
if (before_open) before_open();
if (!root) {
root = lv_obj_create(lv_screen_active());
lv_obj_remove_style_all(root);
lv_obj_set_size(root, lv_pct(100), lv_pct(100));
lv_obj_remove_flag(root, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_add_flag(root, LV_OBJ_FLAG_CLICKABLE); // nothing leaks through to the tiles below
lv_obj_set_style_bg_color(root, lv_color_hex(0xE7E7E7), 0);
lv_obj_set_style_bg_opa(root, LV_OPA_COVER, 0);
}
lv_obj_move_foreground(root);
draw();
}
inline void close() {
forget_confirm();
drawn_count = 0;
if (root) { lv_obj_delete(root); root = nullptr; }
}
// ---- holding the top bar opens the page ----
// Not a tile, not a button in view: a press of `hold_ms` on the strip with the screen name. A line
// grows along the top edge while you hold, so the gesture explains itself the first time it happens.
inline void hold_step(void *object, int32_t value) {
auto *bar = static_cast<lv_obj_t *>(object);
int width = lv_display_get_horizontal_resolution(lv_display_get_default());
if (drift && drift() > hold_limit) { hide_hold_bar(); return; }
// The first quarter second stays invisible: a tap must not flash a line across the screen.
if (value < 250) return;
lv_obj_remove_flag(bar, LV_OBJ_FLAG_HIDDEN);
lv_obj_set_width(bar, std::max<int32_t>(1, (int32_t) width * (value - 250) / (int32_t) (hold_ms - 250)));
}
inline void hold_event(lv_event_t *event) {
auto code = lv_event_get_code(event);
if (!hold_bar) return;
if (code == LV_EVENT_PRESSED) {
if (visible() || (may_open && !may_open())) return;
lv_obj_set_width(hold_bar, 1);
lv_anim_t anim;
lv_anim_init(&anim);
lv_anim_set_var(&anim, hold_bar);
lv_anim_set_values(&anim, 0, (int32_t) hold_ms);
lv_anim_set_duration(&anim, (uint32_t) hold_ms);
lv_anim_set_exec_cb(&anim, hold_step);
lv_anim_set_completed_cb(&anim, [](lv_anim_t *) {
hide_hold_bar();
if (drift && drift() > hold_limit) return;
if (may_open && !may_open()) return;
open();
});
lv_anim_start(&anim);
return;
}
hide_hold_bar();
}
// A transparent strip over the top bar plus the line that fills while it is held. Both belong to the
// page below, so they survive every redraw of the tiles.
inline void attach_hold(lv_obj_t *page, int x, int y, int width, int height) {
bool large = lv_display_get_horizontal_resolution(lv_display_get_default()) >= 480;
hold_area = plain(page, x, y, width, height);
lv_obj_add_flag(hold_area, LV_OBJ_FLAG_CLICKABLE);
// No pressed style at all: a repaint of a strip this wide costs a frame that the touch polling on a
// resistive panel needs (the lesson of firmware 0.2.35).
lv_obj_add_event_cb(hold_area, hold_event, LV_EVENT_PRESSED, nullptr);
lv_obj_add_event_cb(hold_area, hold_event, LV_EVENT_RELEASED, nullptr);
lv_obj_add_event_cb(hold_area, hold_event, LV_EVENT_PRESS_LOST, nullptr);
hold_bar = plain(page, 0, 0, 1, large ? 5 : 3);
lv_obj_set_style_bg_opa(hold_bar, LV_OPA_COVER, 0);
lv_obj_set_style_bg_color(hold_bar, lv_color_hex(0x009FE3), 0);
lv_obj_add_flag(hold_bar, LV_OBJ_FLAG_HIDDEN);
lv_obj_move_foreground(hold_bar);
}
} // namespace settings_screen
#endif
@@ -187,6 +187,9 @@ inline constexpr Named NAMES[] = {
{"weather-sunset-down", 0xF059B},
{"dots-horizontal", 0xF01D8},
{"arrow-oscillating", 0xF1C91},
{"monitor", 0xF0379},
{"information-outline", 0xF02FD},
{"restart", 0xF0709},
};
inline constexpr size_t NAME_COUNT = sizeof(NAMES) / sizeof(NAMES[0]);
// Codepoint of a name the fonts carry; 0 for any other name.