mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
feat(ui-lvgl): roadmap stage 6 - light motion, unified style, accent picker, splash
- Anim.h: screens emerge from the middle (background cover fades, content drifts 6 px), Home pages slide, popups and toasts rise and fade in - Theme.h: accent rules (fill = primary action, dim fill = selected, text = names/counts/modes), theme layer over the default theme, radius tokens, accent colour picker (Settings > Display & power > LOOK, NVS mc_ui) - Splash with the Solo and upstream versions - Status icons in equal cells, L1-style relay marks, path window with a hop diagram, tile download popup fits the screen (WiFi only in Settings) - Perf: uncompressed fonts, two 120-line PSRAM buffers (~48 -> ~33 ms per transition frame); UI_PERF_TEST harness Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -141,7 +141,7 @@ void UITask::buildAdmin() {
|
||||
lv_obj_remove_event_cb(s_pw, onWifiField);
|
||||
lv_textarea_set_max_length(s_pw, 15);
|
||||
lv_obj_add_state(s_pw, LV_STATE_FOCUSED);
|
||||
s_kb = kb::create(lv_screen_active(), _prefs);
|
||||
s_kb = kb::create(screen(), _prefs);
|
||||
lv_obj_set_size(s_kb, LV_PCT(100), 124);
|
||||
lv_obj_align(s_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_keyboard_set_textarea(s_kb, s_pw);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#pragma once
|
||||
// Light motion for ui-lvgl: screens emerge from the middle, popups and toasts
|
||||
// rise and fade in, Home pages slide after a swipe (buttons giving under the
|
||||
// finger is a Theme.h style). Short (under 200 ms) and eased, so nothing waits
|
||||
// on them.
|
||||
|
||||
namespace anim {
|
||||
static const uint32_t SCREEN_MS = 180; // screen change
|
||||
static const uint32_t POP_MS = 160; // popup / toast in
|
||||
static const uint32_t OUT_MS = 120; // toast out
|
||||
static const uint32_t PAGE_MS = 160; // Home page after a swipe
|
||||
|
||||
static void setTy(void* o, int32_t v) { lv_obj_set_style_translate_y((lv_obj_t*)o, v, 0); }
|
||||
static void setTx(void* o, int32_t v) { lv_obj_set_style_translate_x((lv_obj_t*)o, v, 0); }
|
||||
static void setOpa(void* o, int32_t v) { lv_obj_set_style_opa((lv_obj_t*)o, (lv_opa_t)v, 0); }
|
||||
static void setBgOpa(void* o, int32_t v) { lv_obj_set_style_bg_opa((lv_obj_t*)o, (lv_opa_t)v, 0); }
|
||||
|
||||
static void run(lv_obj_t* o, lv_anim_exec_xcb_t cb, int32_t from, int32_t to, uint32_t ms,
|
||||
lv_anim_completed_cb_t done = nullptr) {
|
||||
lv_anim_t a;
|
||||
lv_anim_init(&a);
|
||||
lv_anim_set_var(&a, o);
|
||||
lv_anim_set_exec_cb(&a, cb);
|
||||
lv_anim_set_values(&a, from, to);
|
||||
lv_anim_set_duration(&a, ms);
|
||||
lv_anim_set_path_cb(&a, lv_anim_path_ease_out);
|
||||
if (done) lv_anim_set_completed_cb(&a, done);
|
||||
lv_anim_start(&a);
|
||||
cb(o, from); // no frame of the end state before the first tick
|
||||
}
|
||||
|
||||
// Rise by dy and fade in (a popup's panel, a toast).
|
||||
static void rise(lv_obj_t* o, int32_t dy = 16, uint32_t ms = POP_MS) {
|
||||
run(o, setTy, dy, 0, ms);
|
||||
run(o, setOpa, LV_OPA_TRANSP, LV_OPA_COVER, ms);
|
||||
}
|
||||
|
||||
// A popup: the dimmed backdrop fades up to `dim`, its first child (the
|
||||
// panel) rises.
|
||||
static void popup(lv_obj_t* overlay, lv_opa_t dim = LV_OPA_60) {
|
||||
run(overlay, setBgOpa, LV_OPA_TRANSP, dim, POP_MS);
|
||||
lv_obj_t* panel = lv_obj_get_child(overlay, 0);
|
||||
if (panel) rise(panel);
|
||||
}
|
||||
|
||||
// A new screen emerges: a cover in the background colour fades away over
|
||||
// it while its content drifts a few pixels into place (up going deeper,
|
||||
// down coming back). A plain fill blended on top -- far cheaper than
|
||||
// fading the screen itself (a full-screen layer) or sliding two screens.
|
||||
static void coverDone(lv_anim_t* a) { lv_obj_delete((lv_obj_t*)a->var); }
|
||||
static void screenIn(lv_obj_t* scr, lv_obj_t* body, bool back) {
|
||||
lv_obj_t* cover = lv_obj_create(scr);
|
||||
lv_obj_remove_style_all(cover);
|
||||
lv_obj_remove_flag(cover, LV_OBJ_FLAG_CLICKABLE); // taps go through to the screen
|
||||
lv_obj_add_flag(cover, LV_OBJ_FLAG_IGNORE_LAYOUT);
|
||||
lv_obj_set_size(cover, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_bg_color(cover, lv_obj_get_style_bg_color(scr, 0), 0);
|
||||
run(cover, setBgOpa, LV_OPA_COVER, LV_OPA_TRANSP, SCREEN_MS, coverDone);
|
||||
if (body) run(body, setTy, back ? -6 : 6, 0, SCREEN_MS);
|
||||
}
|
||||
|
||||
// Content that replaced other content in place: slides in from dx.
|
||||
static void slideIn(lv_obj_t* o, int32_t dx, uint32_t ms = PAGE_MS) {
|
||||
run(o, setTx, dx, 0, ms);
|
||||
run(o, setOpa, LV_OPA_40, LV_OPA_COVER, ms);
|
||||
}
|
||||
|
||||
static void hideDone(lv_anim_t* a) {
|
||||
lv_obj_t* o = (lv_obj_t*)a->var;
|
||||
lv_obj_add_flag(o, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_obj_set_style_opa(o, LV_OPA_COVER, 0);
|
||||
}
|
||||
// Fade out, then hide (the object stays for reuse).
|
||||
static void fadeHide(lv_obj_t* o) {
|
||||
lv_anim_delete(o, setOpa);
|
||||
run(o, setOpa, lv_obj_get_style_opa(o, 0), LV_OPA_TRANSP, OUT_MS, hideDone);
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ static void botChip(const char* ph, void* parent) {
|
||||
lv_obj_set_height(b, 26);
|
||||
lv_obj_set_style_pad_hor(b, 7, 0);
|
||||
lv_obj_set_style_pad_ver(b, 0, 0);
|
||||
lv_obj_set_style_radius(b, 13, 0);
|
||||
lv_obj_set_style_radius(b, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_shadow_width(b, 0, 0);
|
||||
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 0);
|
||||
lv_obj_add_event_cb(b, onBotPlaceholder, LV_EVENT_CLICKED, NULL);
|
||||
|
||||
@@ -230,6 +230,7 @@ void UITask::buildChannelEdit() {
|
||||
lv_obj_set_style_bg_color(save, lv_color_hex(theme::ACCENT_DIM), 0);
|
||||
lv_obj_add_event_cb(save, onChanSave, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_center(label(save, LV_SYMBOL_OK " Save", THEME_FONT_BODY, theme::TEXT));
|
||||
stylePrimary(save);
|
||||
s_status = label(body, "", THEME_FONT_SMALL, theme::FAIL);
|
||||
lv_label_set_long_mode(s_status, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(s_status, LV_PCT(100));
|
||||
@@ -244,7 +245,7 @@ void UITask::buildChannelEdit() {
|
||||
lv_label_set_text_fmt(s_hint, "Key: %s", hex);
|
||||
}
|
||||
|
||||
s_kb = kb::create(lv_screen_active(), _prefs);
|
||||
s_kb = kb::create(screen(), _prefs);
|
||||
lv_obj_set_size(s_kb, LV_PCT(100), 124);
|
||||
lv_obj_align(s_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_obj_add_event_cb(s_kb, onChanKb, LV_EVENT_READY, NULL);
|
||||
@@ -303,7 +304,7 @@ void UITask::channelEditField(lv_obj_t* ta) {
|
||||
lv_obj_remove_flag(s_kb, LV_OBJ_FLAG_HIDDEN);
|
||||
if (_body) lv_obj_set_height(_body, lv_obj_get_height(_body) - lv_obj_get_height(s_kb));
|
||||
}
|
||||
lv_obj_update_layout(lv_screen_active());
|
||||
lv_obj_update_layout(screen());
|
||||
lv_obj_scroll_to_view(ta, LV_ANIM_OFF);
|
||||
}
|
||||
|
||||
|
||||
@@ -202,6 +202,7 @@ void UITask::buildClock() {
|
||||
lv_obj_add_event_cb(go, onClockAct, LV_EVENT_CLICKED, (void*)(uintptr_t)ACT_START_STOP);
|
||||
s_go_lbl = label(go, "", THEME_FONT_BODY, theme::TEXT);
|
||||
lv_obj_center(s_go_lbl);
|
||||
stylePrimary(go);
|
||||
if (s_tab == TAB_STOPWATCH) {
|
||||
lv_obj_t* rs = lv_button_create(acts);
|
||||
lv_obj_set_size(rs, 100, 40);
|
||||
@@ -292,6 +293,7 @@ void UITask::showRing(const char* text) {
|
||||
lv_obj_set_style_bg_color(b, lv_color_hex(theme::ACCENT_DIM), 0);
|
||||
lv_obj_add_event_cb(b, onRingDismiss, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_center(label(b, "Dismiss", THEME_FONT_LARGE, theme::TEXT));
|
||||
stylePrimary(b);
|
||||
}
|
||||
lv_label_set_text(s_ring_lbl, text);
|
||||
lv_obj_remove_flag(s_ring, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
@@ -227,6 +227,35 @@ void UITask::conversationAction(uint8_t act) {
|
||||
|
||||
// ── Message actions ───────────────────────────────────────────────────────────
|
||||
|
||||
// One stop on the path diagram: a dot on a vertical line (the line runs on to
|
||||
// the next stop unless `last`), the name beside it.
|
||||
static void pathStop(lv_obj_t* parent, const char* name, bool first, bool last, uint32_t col) {
|
||||
lv_obj_t* row = lv_obj_create(parent);
|
||||
lv_obj_remove_style_all(row);
|
||||
lv_obj_remove_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_size(row, LV_PCT(100), 20);
|
||||
if (!first || !last) {
|
||||
lv_obj_t* line = lv_obj_create(row);
|
||||
lv_obj_remove_style_all(line);
|
||||
lv_obj_set_style_bg_opa(line, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_bg_color(line, lv_color_hex(theme::SURFACE_2), 0);
|
||||
lv_obj_set_size(line, 2, first || last ? 10 : 20);
|
||||
lv_obj_set_pos(line, 6, first ? 10 : 0);
|
||||
}
|
||||
lv_obj_t* dot = lv_obj_create(row);
|
||||
lv_obj_remove_style_all(dot);
|
||||
lv_obj_set_style_bg_opa(dot, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_bg_color(dot, lv_color_hex(col), 0);
|
||||
lv_obj_set_style_radius(dot, LV_RADIUS_CIRCLE, 0);
|
||||
bool end = first || last;
|
||||
lv_obj_set_size(dot, end ? 10 : 8, end ? 10 : 8);
|
||||
lv_obj_align(dot, LV_ALIGN_LEFT_MID, end ? 2 : 3, 0);
|
||||
lv_obj_t* t = label(row, name, end ? THEME_FONT_BODY : THEME_FONT_SMALL, end ? theme::TEXT : theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(t, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_width(t, 240);
|
||||
lv_obj_align(t, LV_ALIGN_LEFT_MID, 22, 0);
|
||||
}
|
||||
|
||||
void UITask::messageMenu(int idx) {
|
||||
using namespace convview;
|
||||
if (idx < 0 || idx >= s_msg_meta_n) return;
|
||||
@@ -234,34 +263,93 @@ void UITask::messageMenu(int idx) {
|
||||
s_msg = idx;
|
||||
lv_obj_t* panel = navPopupPanel(m.own ? "My message" : (m.from[0] ? m.from : "Message"), false);
|
||||
|
||||
// The way it came: hops of an incoming message, or the repeaters heard
|
||||
// relaying our own channel post.
|
||||
const MessageHistory& h = _core->history;
|
||||
uint8_t packed = 0; const uint8_t* path = nullptr; bool relay = false;
|
||||
uint32_t ts = 0; const char* body = "";
|
||||
AckState dm_st = ACK_NONE;
|
||||
if (m.pos >= 0) {
|
||||
if (m.channel) { const ChHistEntry& e = h.chAtPos(m.pos); packed = e.path_len; path = e.path; relay = e.relay_seq != 0; }
|
||||
else { const DmHistEntry& e = h.dmAtPos(m.pos); packed = e.path_len; path = e.path; }
|
||||
if (m.channel) {
|
||||
const ChHistEntry& e = h.chAtPos(m.pos);
|
||||
packed = e.path_len; path = e.path; relay = m.own; ts = e.timestamp;
|
||||
const char* sep = strstr(e.text, ": ");
|
||||
body = sep ? sep + 2 : e.text;
|
||||
} else {
|
||||
const DmHistEntry& e = h.dmAtPos(m.pos);
|
||||
packed = e.path_len; path = e.path; ts = e.timestamp; body = e.text;
|
||||
if (e.outgoing) dm_st = h.dmEffectiveStatus(e);
|
||||
else {
|
||||
ContactInfo tc; // a room post is filed "Author: text"
|
||||
if (MessageHistory::contactByPrefix(_thread_key, tc) && tc.type == ADV_TYPE_ROOM) {
|
||||
const char* sep = strstr(e.text, ": ");
|
||||
if (sep) body = sep + 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The message itself, quoted short, and when.
|
||||
lv_obj_t* q = label(panel, body, THEME_FONT_SMALL, theme::TEXT);
|
||||
lv_label_set_long_mode(q, LV_LABEL_LONG_DOT);
|
||||
lv_obj_set_width(q, LV_PCT(100));
|
||||
lv_obj_set_style_max_height(q, 34, 0); // two lines
|
||||
lv_obj_set_style_border_side(q, LV_BORDER_SIDE_LEFT, 0);
|
||||
lv_obj_set_style_border_width(q, 2, 0);
|
||||
lv_obj_set_style_border_color(q, lv_color_hex(m.own ? theme::ACCENT : theme::SURFACE_2), 0);
|
||||
lv_obj_set_style_pad_left(q, 8, 0);
|
||||
char when[64], age[16];
|
||||
uint32_t now = rtc_clock.getCurrentTime();
|
||||
geo::fmtAgeShort(age, sizeof(age), now, ts ? ts : now);
|
||||
if (ts > 1000000000UL) {
|
||||
time_t t = (time_t)((int64_t)ts + (int64_t)(_prefs ? _prefs->tz_offset_hours : 0) * 3600);
|
||||
struct tm ti = *gmtime(&t);
|
||||
char clk[12];
|
||||
fmtClock(clk, sizeof(clk), ti, _prefs, true);
|
||||
snprintf(when, sizeof(when), "%s - %s ago", clk, age);
|
||||
} else {
|
||||
snprintf(when, sizeof(when), "%s ago", age);
|
||||
}
|
||||
uint8_t hops = contactctl::hopCount(packed);
|
||||
char text[200];
|
||||
int o = 0;
|
||||
if (hops > 0) {
|
||||
o = snprintf(text, sizeof(text), relay ? "Relayed by: " : "Path (%u hop%s): ", hops, hops == 1 ? "" : "s");
|
||||
for (uint8_t i = 0; i < hops && o < (int)sizeof(text) - 40; i++) {
|
||||
if (!m.own) { // incoming: how far it came, on the same line
|
||||
size_t o = strlen(when);
|
||||
if (hops > 0) snprintf(when + o, sizeof(when) - o, " - %u hop%s", hops, hops == 1 ? "" : "s");
|
||||
else snprintf(when + o, sizeof(when) - o, " - direct");
|
||||
}
|
||||
label(panel, when, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
|
||||
if (m.own && !m.channel) { // a DM we sent: its end-to-end delivery, in words
|
||||
const char* st = "Sent"; uint32_t col = theme::TEXT_MUTED;
|
||||
switch (dm_st) {
|
||||
case ACK_OK: st = LV_SYMBOL_OK " Delivered"; col = theme::OK; break;
|
||||
case ACK_FAIL: st = LV_SYMBOL_CLOSE " Not delivered"; col = theme::FAIL; break;
|
||||
case ACK_PENDING: st = "Sending..."; break;
|
||||
default: break;
|
||||
}
|
||||
label(panel, st, THEME_FONT_BODY, col);
|
||||
} else if (relay) { // our channel post: the repeaters heard passing it on
|
||||
char hd[40];
|
||||
if (hops > 0) snprintf(hd, sizeof(hd), LV_SYMBOL_OK " Relayed by %u repeater%s", hops, hops == 1 ? "" : "s");
|
||||
else snprintf(hd, sizeof(hd), "No repeater heard relaying it yet");
|
||||
label(panel, hd, THEME_FONT_BODY, hops > 0 ? theme::OK : theme::TEXT_MUTED);
|
||||
for (uint8_t i = 0; i < hops; i++) {
|
||||
char nm[33], line[40];
|
||||
contactctl::hopName(packed, path, i, nm, sizeof(nm));
|
||||
snprintf(line, sizeof(line), UI_SYMBOL_RADIO " %s", nm);
|
||||
label(panel, line, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
}
|
||||
} else { // incoming: sender, each hop, then us
|
||||
lv_obj_t* diag = lv_obj_create(panel);
|
||||
lv_obj_remove_style_all(diag);
|
||||
lv_obj_remove_flag(diag, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_size(diag, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(diag, LV_FLEX_FLOW_COLUMN);
|
||||
pathStop(diag, m.from[0] ? m.from : "Sender", true, false, theme::ACCENT);
|
||||
for (uint8_t i = 0; i < hops; i++) {
|
||||
char nm[33];
|
||||
contactctl::hopName(packed, path, i, nm, sizeof(nm));
|
||||
o += snprintf(text + o, sizeof(text) - o, "%s%s", i ? (relay ? ", " : " > ") : "", nm);
|
||||
pathStop(diag, nm, false, false, theme::TEXT_MUTED);
|
||||
}
|
||||
} else if (!m.own) {
|
||||
snprintf(text, sizeof(text), "Heard directly (no repeaters)");
|
||||
} else if (m.channel) {
|
||||
snprintf(text, sizeof(text), "No repeater heard relaying it yet");
|
||||
} else {
|
||||
snprintf(text, sizeof(text), "Delivery is shown under the message"); // no path kept for our own DMs
|
||||
pathStop(diag, the_mesh.getNodeName(), false, true, theme::OK);
|
||||
}
|
||||
lv_obj_t* t = label(panel, text, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_set_width(t, LV_PCT(100));
|
||||
|
||||
bool can_reply = !m.own && m.from[0] && _compose_ta;
|
||||
if (!can_reply && m.loc < 0) return;
|
||||
|
||||
@@ -220,7 +220,7 @@ void UITask::lockScreen() {
|
||||
lv_obj_set_style_anim_duration(s_lock_slider, 0, 0); // no lag behind the finger
|
||||
lv_obj_set_style_bg_color(s_lock_slider, lv_color_hex(theme::SURFACE), LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_opa(s_lock_slider, LV_OPA_COVER, LV_PART_MAIN);
|
||||
lv_obj_set_style_radius(s_lock_slider, 22, LV_PART_MAIN);
|
||||
lv_obj_set_style_radius(s_lock_slider, LV_RADIUS_CIRCLE, LV_PART_MAIN);
|
||||
lv_obj_set_style_bg_opa(s_lock_slider, LV_OPA_TRANSP, LV_PART_INDICATOR);
|
||||
lv_obj_set_style_bg_color(s_lock_slider, lv_color_hex(theme::ACCENT), LV_PART_KNOB);
|
||||
lv_obj_set_style_pad_all(s_lock_slider, 2, LV_PART_KNOB);
|
||||
@@ -388,6 +388,51 @@ void UITask::pinRowRefresh() {
|
||||
if (_body) { lv_obj_update_layout(_body); lv_obj_scroll_by(_body, 0, -lv_obj_get_scroll_bottom(_body), LV_ANIM_OFF); }
|
||||
}
|
||||
|
||||
// ── Accent colour ─────────────────────────────────────────────────────────────
|
||||
|
||||
static void onAccent(lv_event_t* e) { s_ui->setAccent((int)(uintptr_t)lv_event_get_user_data(e)); }
|
||||
|
||||
// A row of colour swatches; the current one ringed.
|
||||
void UITask::accentRow(lv_obj_t* body) {
|
||||
lv_obj_t* card = lv_obj_create(body);
|
||||
styleSurface(card, theme::SURFACE);
|
||||
lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_size(card, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_style_radius(card, theme::RADIUS, 0);
|
||||
lv_obj_set_style_pad_all(card, theme::PAD, 0);
|
||||
lv_obj_set_style_pad_row(card, 8, 0);
|
||||
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||
label(card, "Accent colour", THEME_FONT_BODY, theme::TEXT);
|
||||
lv_obj_t* row = lv_obj_create(card);
|
||||
lv_obj_remove_style_all(row);
|
||||
lv_obj_set_size(row, LV_PCT(100), 36);
|
||||
lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(row, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
for (int i = 0; i < theme::ACCENT_COUNT; i++) {
|
||||
lv_obj_t* sw = lv_button_create(row);
|
||||
lv_obj_set_size(sw, 32, 32);
|
||||
lv_obj_set_style_radius(sw, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_bg_color(sw, lv_color_hex(theme::ACCENTS[i].col), 0);
|
||||
bool cur = i == theme::s_accent;
|
||||
lv_obj_set_style_border_width(sw, cur ? 3 : 0, 0);
|
||||
lv_obj_set_style_border_color(sw, lv_color_hex(theme::TEXT), 0);
|
||||
lv_obj_add_event_cb(sw, onAccent, LV_EVENT_CLICKED, (void*)(uintptr_t)i);
|
||||
if (cur) lv_obj_center(label(sw, LV_SYMBOL_OK, THEME_FONT_SMALL, theme::BG));
|
||||
}
|
||||
}
|
||||
|
||||
// Recolours everything: the theme, the status bar and toast, then this page.
|
||||
void UITask::setAccent(int idx) {
|
||||
if (idx == theme::s_accent) return;
|
||||
theme::setAccent(idx);
|
||||
lvport::saveAccent(theme::s_accent);
|
||||
theme::install(lv_display_get_default());
|
||||
if (_toast) lv_obj_set_style_border_color(_toast, lv_color_hex(theme::ACCENT), 0);
|
||||
_status_sig[0] = '\0'; // icons in the accent rebuilt
|
||||
refreshStatusBar();
|
||||
pinRowRefresh(); // the page, kept at its bottom where this row is
|
||||
}
|
||||
|
||||
// ── Favourites dial ───────────────────────────────────────────────────────────
|
||||
|
||||
void UITask::showFavourites() {
|
||||
|
||||
@@ -29,14 +29,21 @@ static const int WIFI_SCAN_MAX = 12;
|
||||
|
||||
static lgfx::LGFX_Device* s_gfx = nullptr;
|
||||
static bool s_swallow = false; // ignore the touch that woke the display until it lifts
|
||||
static uint32_t s_flush_us = 0; // UI_PERF_TEST: time spent in flushCb
|
||||
|
||||
static void flushCb(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map) {
|
||||
#ifdef UI_PERF_TEST
|
||||
uint32_t t = micros();
|
||||
#endif
|
||||
int w = area->x2 - area->x1 + 1;
|
||||
int h = area->y2 - area->y1 + 1;
|
||||
s_gfx->startWrite();
|
||||
s_gfx->setAddrWindow(area->x1, area->y1, w, h);
|
||||
s_gfx->pushPixels((uint16_t*)px_map, (uint32_t)w * h, true /* LVGL RGB565 is little-endian */);
|
||||
s_gfx->endWrite();
|
||||
#ifdef UI_PERF_TEST
|
||||
s_flush_us += micros() - t;
|
||||
#endif
|
||||
lv_display_flush_ready(disp);
|
||||
}
|
||||
|
||||
@@ -65,8 +72,11 @@ static bool begin() {
|
||||
lv_display_t* disp = lv_display_create(s_gfx->width(), s_gfx->height());
|
||||
lv_display_set_flush_cb(disp, flushCb);
|
||||
|
||||
// Two 40-line partial buffers (25 KB each), PSRAM first.
|
||||
const uint32_t buf_sz = (uint32_t)s_gfx->width() * 40 * 2;
|
||||
// Two half-screen buffers (75 KB each) in PSRAM: a frame renders in two
|
||||
// passes instead of six (every pass walks the whole tree and lays text out
|
||||
// again) -- measured ~8% faster than 40 lines; internal RAM / DMA / -O2
|
||||
// made no difference (UI_PERF_TEST).
|
||||
const uint32_t buf_sz = (uint32_t)s_gfx->width() * 120 * 2;
|
||||
uint8_t* buf1 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_SPIRAM);
|
||||
uint8_t* buf2 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_SPIRAM);
|
||||
if (!buf1) { buf1 = (uint8_t*)heap_caps_malloc(buf_sz, MALLOC_CAP_8BIT); buf2 = nullptr; }
|
||||
@@ -168,6 +178,21 @@ static void savePin(const char* pin) {
|
||||
p.end();
|
||||
}
|
||||
|
||||
// Accent colour (Settings > Display & power): an index into theme::ACCENTS.
|
||||
static int loadAccent() {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_ui", true)) return 0;
|
||||
int v = p.getUChar("accent", 0);
|
||||
p.end();
|
||||
return v;
|
||||
}
|
||||
static void saveAccent(int idx) {
|
||||
Preferences p;
|
||||
if (!p.begin("mc_ui", false)) return;
|
||||
p.putUChar("accent", (uint8_t)idx);
|
||||
p.end();
|
||||
}
|
||||
|
||||
static void netBegin(const char* ssid, const char* pass) {
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(ssid, pass);
|
||||
@@ -398,6 +423,9 @@ static void saveWifi(const char* ssid, const char* pass) {
|
||||
static char s_pin[9] = ""; // the screen PIN, for the session
|
||||
static void loadPin(char* out, size_t n) { snprintf(out, n, "%s", s_pin); }
|
||||
static void savePin(const char* pin) { snprintf(s_pin, sizeof(s_pin), "%s", pin); }
|
||||
static int s_accent = 0;
|
||||
static int loadAccent() { return s_accent; }
|
||||
static void saveAccent(int idx) { s_accent = idx; }
|
||||
static bool s_wifi_on = true;
|
||||
static bool wifiAllowed() { return s_wifi_on; }
|
||||
static void setWifiAllowed(bool on) { s_wifi_on = on; }
|
||||
|
||||
@@ -149,7 +149,6 @@ static void onDlClose(lv_event_t* e) { (void)e; s_ui->mapDownloadClose(); }
|
||||
static void onDlStart(lv_event_t* e) { (void)e; s_ui->mapDownloadStart(); }
|
||||
static void onDlZoomMinus(lv_event_t* e) { (void)e; s_ui->mapDownloadZmax(-1); }
|
||||
static void onDlZoomPlus(lv_event_t* e) { (void)e; s_ui->mapDownloadZmax(+1); }
|
||||
static void onDlWifi(lv_event_t* e) { (void)e; s_ui->showWifi(true); }
|
||||
static void onDlResume(lv_event_t* e) { (void)e; s_ui->mapDownloadResume(); }
|
||||
static void onDlDiscard(lv_event_t* e) { (void)e; s_ui->mapDownloadDiscard(); }
|
||||
static void onNavTools(lv_event_t* e); // NavMap.h
|
||||
@@ -580,7 +579,7 @@ void UITask::mapDownloadPopup() {
|
||||
if (_dl_overlay) return;
|
||||
if (_dl_zmax < _map_z) _dl_zmax = _map_z + 3 > 17 ? 17 : _map_z + 3;
|
||||
|
||||
_dl_overlay = lv_obj_create(lv_screen_active());
|
||||
_dl_overlay = lv_obj_create(screen());
|
||||
lv_obj_remove_style_all(_dl_overlay);
|
||||
lv_obj_set_size(_dl_overlay, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_bg_color(_dl_overlay, lv_color_hex(0x000000), 0);
|
||||
@@ -589,6 +588,7 @@ void UITask::mapDownloadPopup() {
|
||||
lv_obj_remove_flag(_dl_overlay, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
lv_obj_t* panel = lv_obj_create(_dl_overlay);
|
||||
anim::popup(_dl_overlay);
|
||||
lv_obj_set_size(panel, lv_display_get_horizontal_resolution(NULL) - 16, LV_SIZE_CONTENT);
|
||||
lv_obj_align(panel, LV_ALIGN_CENTER, 0, theme::STATUS_H / 2);
|
||||
lv_obj_set_style_bg_color(panel, lv_color_hex(theme::BG), 0);
|
||||
@@ -598,7 +598,7 @@ void UITask::mapDownloadPopup() {
|
||||
lv_obj_set_style_pad_all(panel, theme::PAD, 0);
|
||||
lv_obj_set_style_pad_row(panel, 6, 0);
|
||||
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_remove_flag(panel, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_set_style_max_height(panel, lv_display_get_vertical_resolution(NULL) - theme::STATUS_H - 12, 0); // scrolls past that
|
||||
|
||||
lv_obj_t* hdr = lv_obj_create(panel);
|
||||
styleSurface(hdr, theme::BG);
|
||||
@@ -628,8 +628,7 @@ void UITask::mapDownloadPopup() {
|
||||
lv_obj_set_width(_dl_job_lbl, 170);
|
||||
lv_label_set_long_mode(_dl_job_lbl, LV_LABEL_LONG_WRAP);
|
||||
lv_obj_align(_dl_job_lbl, LV_ALIGN_LEFT_MID, theme::PAD, 0);
|
||||
lv_obj_t* res = headerButton(_dl_job_row, LV_SYMBOL_PLAY " Resume", onDlResume, 48, NULL);
|
||||
lv_obj_set_style_bg_color(res, lv_color_hex(theme::ACCENT_DIM), 0);
|
||||
stylePrimary(headerButton(_dl_job_row, LV_SYMBOL_PLAY " Resume", onDlResume, 48, NULL));
|
||||
headerButton(_dl_job_row, LV_SYMBOL_TRASH, onDlDiscard, 4, NULL);
|
||||
lv_obj_add_flag(_dl_job_row, LV_OBJ_FLAG_HIDDEN);
|
||||
|
||||
@@ -644,23 +643,16 @@ void UITask::mapDownloadPopup() {
|
||||
lv_obj_set_size(acts, LV_PCT(100), LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_pad_column(acts, theme::GAP, 0);
|
||||
lv_obj_t* wifi = lv_button_create(acts);
|
||||
lv_obj_set_height(wifi, 38);
|
||||
lv_obj_set_flex_grow(wifi, 1);
|
||||
lv_obj_set_style_shadow_width(wifi, 0, 0);
|
||||
lv_obj_set_style_radius(wifi, theme::RADIUS, 0);
|
||||
lv_obj_set_style_bg_color(wifi, lv_color_hex(theme::SURFACE), 0);
|
||||
lv_obj_add_event_cb(wifi, onDlWifi, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_center(label(wifi, LV_SYMBOL_WIFI " WiFi", THEME_FONT_SMALL, theme::TEXT));
|
||||
lv_obj_t* go = lv_button_create(acts);
|
||||
lv_obj_set_height(go, 38);
|
||||
lv_obj_set_flex_grow(go, 2);
|
||||
lv_obj_set_flex_grow(go, 1);
|
||||
lv_obj_set_style_shadow_width(go, 0, 0);
|
||||
lv_obj_set_style_radius(go, theme::RADIUS, 0);
|
||||
lv_obj_set_style_bg_color(go, lv_color_hex(theme::ACCENT_DIM), 0);
|
||||
lv_obj_add_event_cb(go, onDlStart, LV_EVENT_CLICKED, NULL);
|
||||
_dl_start_lbl = label(go, "", THEME_FONT_SMALL, theme::TEXT);
|
||||
lv_obj_center(_dl_start_lbl);
|
||||
stylePrimary(go);
|
||||
|
||||
refreshDownloadPopup();
|
||||
}
|
||||
@@ -723,7 +715,7 @@ void UITask::refreshDownloadPopup() {
|
||||
else snprintf(size, sizeof(size), "%lu MB", (unsigned long)((bytes + 512 * 1024) / (1024 * 1024)));
|
||||
lv_label_set_text_fmt(_dl_info, "z%d-%d: %lu tiles, about %s - from %s\nWiFi: %s%s",
|
||||
a.zmin, a.zmax, (unsigned long)n, size,
|
||||
dl.sourceHost(), have_wifi ? ssid : "not set - tap WiFi", last);
|
||||
dl.sourceHost(), have_wifi ? ssid : "none - Settings > WiFi", last);
|
||||
lv_label_set_text(_dl_start_lbl, n > mapview::TileDownloader::MAX_TILES ? "Too large - zoom in"
|
||||
: LV_SYMBOL_DOWNLOAD " Download");
|
||||
}
|
||||
@@ -732,7 +724,7 @@ void UITask::mapDownloadStart() {
|
||||
if (mapview::s_dl.active()) { mapDownloadStop(); return; }
|
||||
char ssid[33], pass[65];
|
||||
if (!lvport::wifiAllowed()) { showToast("WiFi is off - Settings > WiFi"); return; }
|
||||
if (!lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass))) { showWifi(true); return; }
|
||||
if (!lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass))) { showToast("Pick a WiFi network first - Settings > WiFi", 3000); return; }
|
||||
if (!lvport::mountStorage()) { showToast("No SD card"); return; }
|
||||
int w = _map_area ? lv_obj_get_width(_map_area) : 320, h = _map_area ? lv_obj_get_height(_map_area) : 218;
|
||||
mapview::TileArea a = visibleArea(_map_cx, _map_cy, _map_z, w, h, dlZmin(_map_z), _dl_zmax);
|
||||
@@ -745,7 +737,7 @@ void UITask::mapDownloadResume() {
|
||||
if (mapview::s_dl.active() || !mapview::s_dl.savedJob(a)) return;
|
||||
char ssid[33], pass[65];
|
||||
if (!lvport::wifiAllowed()) { showToast("WiFi is off - Settings > WiFi"); return; }
|
||||
if (!lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass))) { showWifi(true); return; }
|
||||
if (!lvport::loadWifi(ssid, sizeof(ssid), pass, sizeof(pass))) { showToast("Pick a WiFi network first - Settings > WiFi", 3000); return; }
|
||||
if (!lvport::mountStorage()) { showToast("No SD card"); return; }
|
||||
if (!mapview::s_dl.start(a, ssid, pass)) { showToast(mapview::s_dl.message()); return; }
|
||||
refreshDownloadPopup();
|
||||
|
||||
@@ -440,7 +440,7 @@ void UITask::navToNode(const uint8_t* key, int32_t lat, int32_t lon, const char*
|
||||
|
||||
lv_obj_t* UITask::navPopupPanel(const char* title, bool full) {
|
||||
navClosePopup();
|
||||
_nav_overlay = lv_obj_create(lv_screen_active());
|
||||
_nav_overlay = lv_obj_create(screen());
|
||||
lv_obj_remove_style_all(_nav_overlay);
|
||||
lv_obj_set_size(_nav_overlay, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_bg_color(_nav_overlay, lv_color_hex(0x000000), 0);
|
||||
@@ -449,6 +449,7 @@ lv_obj_t* UITask::navPopupPanel(const char* title, bool full) {
|
||||
lv_obj_remove_flag(_nav_overlay, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
lv_obj_t* panel = lv_obj_create(_nav_overlay);
|
||||
anim::popup(_nav_overlay);
|
||||
int w = lv_display_get_horizontal_resolution(NULL) - 16;
|
||||
if (full) {
|
||||
lv_obj_set_size(panel, w, lv_display_get_vertical_resolution(NULL) - theme::STATUS_H - 12);
|
||||
|
||||
@@ -196,6 +196,7 @@ void UITask::buildOta() {
|
||||
_ota_btn = b;
|
||||
_ota_btn_lbl = label(b, "", THEME_FONT_BODY, theme::TEXT);
|
||||
lv_obj_center(_ota_btn_lbl);
|
||||
stylePrimary(b);
|
||||
lv_obj_t* h = label(body, "From github.com/" OTA_REPO " releases, over WiFi. About a minute; keep the device "
|
||||
"charged. Settings, contacts and messages are kept.", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_label_set_long_mode(h, LV_LABEL_LONG_WRAP);
|
||||
|
||||
@@ -30,7 +30,7 @@ static lv_obj_t* chips(lv_obj_t* parent, lv_event_cb_t cb, bool wrap) {
|
||||
lv_obj_set_size(b, LV_SIZE_CONTENT, 28);
|
||||
lv_obj_set_style_pad_hor(b, 8, 0);
|
||||
lv_obj_set_style_pad_ver(b, 0, 0);
|
||||
lv_obj_set_style_radius(b, 8, 0);
|
||||
lv_obj_set_style_radius(b, theme::RADIUS_SM, 0);
|
||||
lv_obj_set_style_shadow_width(b, 0, 0);
|
||||
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 0);
|
||||
lv_obj_add_event_cb(b, cb, LV_EVENT_CLICKED, (void*)(uintptr_t)i);
|
||||
@@ -128,7 +128,7 @@ void UITask::quickEdit(int slot) {
|
||||
collect();
|
||||
s_edit = slot;
|
||||
navClosePopup();
|
||||
_nav_overlay = lv_obj_create(lv_screen_active());
|
||||
_nav_overlay = lv_obj_create(screen());
|
||||
lv_obj_remove_style_all(_nav_overlay);
|
||||
lv_obj_set_size(_nav_overlay, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_bg_color(_nav_overlay, lv_color_hex(0x000000), 0);
|
||||
|
||||
@@ -170,13 +170,14 @@ void UITask::radioFreqPopup(bool repeater) {
|
||||
using namespace radioview;
|
||||
if (s_overlay) return;
|
||||
s_freq_rpt = repeater;
|
||||
s_overlay = lv_obj_create(lv_screen_active());
|
||||
s_overlay = lv_obj_create(screen());
|
||||
lv_obj_remove_style_all(s_overlay);
|
||||
lv_obj_set_size(s_overlay, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_bg_color(s_overlay, lv_color_hex(0x000000), 0);
|
||||
lv_obj_set_style_bg_opa(s_overlay, LV_OPA_60, 0);
|
||||
lv_obj_add_flag(s_overlay, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_t* panel = lv_obj_create(s_overlay);
|
||||
anim::popup(s_overlay);
|
||||
lv_obj_set_size(panel, lv_display_get_horizontal_resolution(NULL) - 16, LV_SIZE_CONTENT);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_MID, 0, theme::STATUS_H + 4);
|
||||
lv_obj_set_style_bg_color(panel, lv_color_hex(theme::BG), 0);
|
||||
|
||||
@@ -97,7 +97,7 @@ static void rebuildStrip() {
|
||||
lv_obj_t* c = lv_button_create(s_strip);
|
||||
lv_obj_set_size(c, 38, 44);
|
||||
lv_obj_set_style_pad_all(c, 0, 0);
|
||||
lv_obj_set_style_radius(c, 6, 0);
|
||||
lv_obj_set_style_radius(c, theme::RADIUS_SM, 0);
|
||||
lv_obj_set_style_shadow_width(c, 0, 0);
|
||||
lv_obj_set_style_border_color(c, lv_color_hex(theme::ACCENT), 0);
|
||||
lv_obj_add_event_cb(c, onMelNote, LV_EVENT_CLICKED, (void*)(uintptr_t)i);
|
||||
@@ -142,7 +142,7 @@ static lv_obj_t* smallButton(lv_obj_t* parent, const char* text, uint8_t act) {
|
||||
lv_obj_t* b = lv_button_create(parent);
|
||||
lv_obj_set_size(b, 40, 32);
|
||||
lv_obj_set_style_pad_all(b, 0, 0);
|
||||
lv_obj_set_style_radius(b, 8, 0);
|
||||
lv_obj_set_style_radius(b, theme::RADIUS_SM, 0);
|
||||
lv_obj_set_style_shadow_width(b, 0, 0);
|
||||
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE_2), 0);
|
||||
lv_obj_add_event_cb(b, onMelAction, LV_EVENT_CLICKED, (void*)(uintptr_t)act);
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
// Boot splash -- included into UITask.cpp only.
|
||||
//
|
||||
// The MeshCore wordmark (ui-new's 128x13 bitmap, drawn 2x in amber), "SOLO",
|
||||
// the Solo and upstream versions and the build date, with three dots rising
|
||||
// in turn while the rest of the device starts. Covers the whole display
|
||||
// (status bar too) from lv_layer_top, fades out after SPLASH_MS; a tap skips
|
||||
// it. Home -- and the PIN lock, if set -- are already built underneath.
|
||||
|
||||
namespace splash {
|
||||
static const uint32_t SPLASH_MS = 2500;
|
||||
static const int LOGO_W = 128, LOGO_H = 13, LOGO_SCALE = 2;
|
||||
|
||||
// 'meshcore', 128x13 px, MSB first (ui-new/icons.h meshcore_logo)
|
||||
static const uint8_t LOGO[] = {
|
||||
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
|
||||
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
|
||||
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
|
||||
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
|
||||
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
|
||||
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
|
||||
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
|
||||
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
|
||||
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
|
||||
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
|
||||
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
|
||||
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
|
||||
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
|
||||
};
|
||||
|
||||
static lv_obj_t* s_root = nullptr;
|
||||
static uint32_t* s_px = nullptr; // ARGB8888 pixels of the scaled logo
|
||||
static lv_image_dsc_t s_img;
|
||||
static lv_timer_t* s_timer = nullptr;
|
||||
|
||||
static void freeAll(lv_anim_t* a) {
|
||||
(void)a;
|
||||
if (s_root) lv_obj_delete(s_root);
|
||||
s_root = nullptr;
|
||||
if (s_px) lv_free(s_px);
|
||||
s_px = nullptr;
|
||||
}
|
||||
|
||||
static void dismiss() {
|
||||
if (s_timer) { lv_timer_delete(s_timer); s_timer = nullptr; }
|
||||
if (!s_root) return;
|
||||
lv_obj_remove_flag(s_root, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_anim_delete(s_root, NULL);
|
||||
anim::run(s_root, anim::setOpa, LV_OPA_COVER, LV_OPA_TRANSP, 300, freeAll);
|
||||
}
|
||||
static void onTimer(lv_timer_t* t) { (void)t; dismiss(); }
|
||||
static void onTap(lv_event_t* e) { (void)e; dismiss(); }
|
||||
|
||||
// The logo bitmap scaled up into an ARGB8888 image in `col`.
|
||||
static bool buildLogo(uint32_t col) {
|
||||
const int W = LOGO_W * LOGO_SCALE, H = LOGO_H * LOGO_SCALE;
|
||||
s_px = (uint32_t*)lv_malloc((size_t)W * H * 4);
|
||||
if (!s_px) return false;
|
||||
uint32_t on = 0xFF000000u | (col & 0xFFFFFFu);
|
||||
for (int y = 0; y < H; y++)
|
||||
for (int x = 0; x < W; x++) {
|
||||
int sx = x / LOGO_SCALE, sy = y / LOGO_SCALE;
|
||||
bool bit = LOGO[sy * (LOGO_W / 8) + sx / 8] & (0x80 >> (sx % 8));
|
||||
s_px[y * W + x] = bit ? on : 0;
|
||||
}
|
||||
memset(&s_img, 0, sizeof(s_img));
|
||||
s_img.header.magic = LV_IMAGE_HEADER_MAGIC;
|
||||
s_img.header.cf = LV_COLOR_FORMAT_ARGB8888;
|
||||
s_img.header.w = W;
|
||||
s_img.header.h = H;
|
||||
s_img.header.stride = W * 4;
|
||||
s_img.data_size = (uint32_t)W * H * 4;
|
||||
s_img.data = (const uint8_t*)s_px;
|
||||
return true;
|
||||
}
|
||||
|
||||
// One of the loading dots: rises 6 px and back, each a beat after the last.
|
||||
static void dotY(void* o, int32_t v) { lv_obj_set_style_translate_y((lv_obj_t*)o, v, 0); }
|
||||
static void wave(lv_obj_t* dot, int i) {
|
||||
lv_anim_t a;
|
||||
lv_anim_init(&a);
|
||||
lv_anim_set_var(&a, dot);
|
||||
lv_anim_set_exec_cb(&a, dotY);
|
||||
lv_anim_set_values(&a, 0, -6);
|
||||
lv_anim_set_duration(&a, 260);
|
||||
lv_anim_set_playback_duration(&a, 260);
|
||||
lv_anim_set_repeat_count(&a, LV_ANIM_REPEAT_INFINITE);
|
||||
lv_anim_set_repeat_delay(&a, 420);
|
||||
lv_anim_set_delay(&a, 140 * i);
|
||||
lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out);
|
||||
lv_anim_start(&a);
|
||||
}
|
||||
|
||||
// "v1.28-solo.1-abcdef" -> "v1.28-solo.1": build.sh appends the commit as
|
||||
// the last dash segment (as ui-new's splash).
|
||||
static void soloVersion(char* out, size_t n) {
|
||||
const char* ver = FIRMWARE_VERSION;
|
||||
const char* dash = strrchr(ver, '-');
|
||||
size_t len = dash ? (size_t)(dash - ver) : strlen(ver);
|
||||
if (len >= n) len = n - 1;
|
||||
memcpy(out, ver, len);
|
||||
out[len] = '\0';
|
||||
}
|
||||
} // namespace splash
|
||||
|
||||
void UITask::showSplash() {
|
||||
using namespace splash;
|
||||
if (s_root) return;
|
||||
s_root = lv_obj_create(lv_layer_top());
|
||||
lv_obj_remove_style_all(s_root);
|
||||
lv_obj_set_size(s_root, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_bg_color(s_root, lv_color_hex(theme::BG), 0);
|
||||
lv_obj_set_style_bg_opa(s_root, LV_OPA_COVER, 0);
|
||||
lv_obj_add_flag(s_root, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_event_cb(s_root, onTap, LV_EVENT_CLICKED, NULL);
|
||||
|
||||
if (buildLogo(theme::ACCENT)) {
|
||||
lv_obj_t* logo = lv_image_create(s_root);
|
||||
lv_image_set_src(logo, &s_img);
|
||||
lv_obj_align(logo, LV_ALIGN_CENTER, 0, -42);
|
||||
anim::rise(logo, 10, 400);
|
||||
}
|
||||
lv_obj_t* solo = label(s_root, "S O L O", THEME_FONT_TITLE, theme::TEXT);
|
||||
lv_obj_align(solo, LV_ALIGN_CENTER, 0, -8);
|
||||
|
||||
char ver[24], line[64];
|
||||
soloVersion(ver, sizeof(ver));
|
||||
lv_obj_t* v = label(s_root, ver[0] ? ver : "dev", THEME_FONT_BODY, theme::ACCENT);
|
||||
lv_obj_align(v, LV_ALIGN_CENTER, 0, 22);
|
||||
#ifdef MESHCORE_VERSION
|
||||
snprintf(line, sizeof(line), "MeshCore %s - built %s", MESHCORE_VERSION, FIRMWARE_BUILD_DATE);
|
||||
#else
|
||||
snprintf(line, sizeof(line), "Built %s", FIRMWARE_BUILD_DATE);
|
||||
#endif
|
||||
lv_obj_t* up = label(s_root, line, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
lv_obj_align(up, LV_ALIGN_CENTER, 0, 42);
|
||||
|
||||
lv_obj_t* dots = lv_obj_create(s_root);
|
||||
lv_obj_remove_style_all(dots);
|
||||
lv_obj_set_size(dots, 60, 20);
|
||||
lv_obj_align(dots, LV_ALIGN_BOTTOM_MID, 0, -18);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
lv_obj_t* d = lv_obj_create(dots);
|
||||
lv_obj_remove_style_all(d);
|
||||
lv_obj_set_size(d, 7, 7);
|
||||
lv_obj_set_style_radius(d, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_bg_opa(d, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_bg_color(d, lv_color_hex(theme::ACCENT), 0);
|
||||
lv_obj_align(d, LV_ALIGN_BOTTOM_MID, (i - 1) * 16, -2);
|
||||
wave(d, i);
|
||||
}
|
||||
s_timer = lv_timer_create(onTimer, SPLASH_MS, NULL); // deleted by dismiss()
|
||||
}
|
||||
@@ -1,14 +1,24 @@
|
||||
#pragma once
|
||||
// Design tokens for ui-lvgl — the one place colours, spacing and type live.
|
||||
// First colour take on the "Amber Trace" direction: amber on near-black.
|
||||
// Design tokens for ui-lvgl -- the one place colours, spacing and type live --
|
||||
// and the rules every widget follows (install() below).
|
||||
//
|
||||
// The accent (amber by default; Settings > Display & power > Accent colour)
|
||||
// on near-black, soft corners. The accent means one of three things:
|
||||
// - ACCENT fill, dark text: the primary action of a screen or popup
|
||||
// (Download, Install, Save, Go) -- stylePrimary() in UITask.cpp;
|
||||
// - ACCENT_DIM fill, light text: something selected / on (a tab, a chip, a
|
||||
// segment, a mode that is running);
|
||||
// - ACCENT text or icon: a name, a count, a background mode, a value.
|
||||
// Everything else is BG / SURFACE / SURFACE_2 with TEXT or TEXT_MUTED.
|
||||
#include <src/themes/lv_theme_private.h> // lv_theme_t's fields, to layer a theme
|
||||
|
||||
namespace theme {
|
||||
// Colours (0xRRGGBB)
|
||||
static const uint32_t BG = 0x0B0B0D; // screen background
|
||||
static const uint32_t SURFACE = 0x17171B; // cards, rows, bubbles (incoming)
|
||||
static const uint32_t SURFACE_2 = 0x222228; // pressed / raised
|
||||
static const uint32_t ACCENT = 0xFFB000; // amber: focus, badges, own bubbles
|
||||
static const uint32_t ACCENT_DIM= 0x6B4A00; // own bubble fill
|
||||
static const uint32_t SURFACE = 0x1A1A1F; // cards, rows, bubbles (incoming)
|
||||
static const uint32_t SURFACE_2 = 0x27272E; // pressed / raised
|
||||
static uint32_t ACCENT = 0xFFB000; // primary actions, names, badges, modes (setAccent)
|
||||
static uint32_t ACCENT_DIM = 0x6B4A00; // selected / on fill, own bubbles (setAccent)
|
||||
static const uint32_t TEXT = 0xF2EEE6;
|
||||
static const uint32_t TEXT_MUTED= 0x9A958C;
|
||||
static const uint32_t OK = 0x6FCF6F; // delivered / relayed
|
||||
@@ -17,7 +27,8 @@ namespace theme {
|
||||
// Spacing (px)
|
||||
static const int PAD = 8;
|
||||
static const int GAP = 6;
|
||||
static const int RADIUS = 10;
|
||||
static const int RADIUS = 10; // cards, rows, buttons (pills use LV_RADIUS_CIRCLE)
|
||||
static const int RADIUS_SM = 6; // small cells inside those
|
||||
static const int STATUS_H = 22; // top status bar
|
||||
static const int ROW_H = 44; // list row (touch target)
|
||||
|
||||
@@ -42,4 +53,62 @@ namespace theme {
|
||||
#define UI_SYMBOL_CHART "\xEF\x82\x80" // U+F080, diagnostics
|
||||
#define UI_SYMBOL_KEY "\xEF\x82\x84" // U+F084, admin
|
||||
#define UI_SYMBOL_ROUTE "\xEF\x93\x97" // U+F4D7, trail
|
||||
#define UI_SYMBOL_MUTE "\xEF\x9A\xA9" // U+F6A9, muted (speaker with a cross)
|
||||
|
||||
// Accent choices; the fill is the colour at 42% over the background.
|
||||
struct Accent { const char* name; uint32_t col; };
|
||||
static const Accent ACCENTS[] = {
|
||||
{ "Amber", 0xFFB000 }, { "Orange", 0xFF7A2E }, { "Coral", 0xFF6B7A },
|
||||
{ "Violet", 0xB18CFF }, { "Cyan", 0x3CC8E8 }, { "Lime", 0xB5E04A },
|
||||
};
|
||||
static const int ACCENT_COUNT = sizeof(ACCENTS) / sizeof(ACCENTS[0]);
|
||||
static int s_accent = 0;
|
||||
static void setAccent(int idx) {
|
||||
if (idx < 0 || idx >= ACCENT_COUNT) idx = 0;
|
||||
s_accent = idx;
|
||||
ACCENT = ACCENTS[idx].col;
|
||||
uint32_t dim = 0;
|
||||
for (int sh = 0; sh <= 16; sh += 8) {
|
||||
uint32_t c = (ACCENT >> sh) & 0xFF, b = (BG >> sh) & 0xFF;
|
||||
dim |= ((c * 42 + b * 58) / 100) << sh;
|
||||
}
|
||||
ACCENT_DIM = dim;
|
||||
}
|
||||
|
||||
// Over LVGL's default theme, for every widget: a checked button is
|
||||
// "selected" (ACCENT_DIM, not the default theme's full accent), buttons
|
||||
// have the card radius and give a little under the finger (the default
|
||||
// theme's transitions animate it both ways).
|
||||
static lv_theme_t s_theme;
|
||||
static lv_style_t s_btn, s_press, s_checked;
|
||||
static void applyTheme(lv_theme_t* th, lv_obj_t* obj) {
|
||||
(void)th;
|
||||
if (lv_obj_check_type(obj, &lv_button_class)) {
|
||||
lv_obj_add_style(obj, &s_btn, 0);
|
||||
lv_obj_add_style(obj, &s_press, LV_STATE_PRESSED);
|
||||
lv_obj_add_style(obj, &s_checked, LV_STATE_CHECKED);
|
||||
}
|
||||
}
|
||||
// (Re)builds the default theme in the current accent with this layer over it.
|
||||
static void install(lv_display_t* disp) {
|
||||
static bool styled = false;
|
||||
if (!styled) {
|
||||
styled = true;
|
||||
lv_style_init(&s_btn);
|
||||
lv_style_set_radius(&s_btn, RADIUS);
|
||||
lv_style_set_shadow_width(&s_btn, 0);
|
||||
lv_style_init(&s_press);
|
||||
lv_style_set_transform_width(&s_press, -2);
|
||||
lv_style_set_transform_height(&s_press, -2);
|
||||
lv_style_init(&s_checked);
|
||||
lv_style_set_text_color(&s_checked, lv_color_hex(TEXT));
|
||||
}
|
||||
lv_style_set_bg_color(&s_checked, lv_color_hex(ACCENT_DIM));
|
||||
lv_theme_t* base = lv_theme_default_init(disp, lv_color_hex(ACCENT), lv_color_hex(ACCENT_DIM), true, THEME_FONT_BODY);
|
||||
s_theme = *base;
|
||||
lv_theme_set_parent(&s_theme, base);
|
||||
lv_theme_set_apply_cb(&s_theme, applyTheme);
|
||||
lv_display_set_theme(disp, &s_theme);
|
||||
lv_obj_report_style_change(&s_checked);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ template <class T> static T* psramBuf(size_t n) {
|
||||
#include "../ui-core/SoundNotifier.h"
|
||||
#include "../ui-core/MessageText.h"
|
||||
#include "Theme.h"
|
||||
#include "Anim.h"
|
||||
#include "LvglPort.h"
|
||||
#include "../ui-core/KeyboardData.h"
|
||||
#include "Keyboard.h"
|
||||
@@ -93,7 +94,7 @@ static lv_obj_t* segmented(lv_obj_t* parent, const char** map, int sel, int w, i
|
||||
lv_obj_set_style_text_color(m, lv_color_hex(theme::TEXT), LV_PART_ITEMS);
|
||||
lv_obj_set_style_text_font(m, THEME_FONT_SMALL, LV_PART_ITEMS);
|
||||
lv_obj_set_style_shadow_width(m, 0, LV_PART_ITEMS);
|
||||
lv_obj_set_style_radius(m, 8, LV_PART_ITEMS);
|
||||
lv_obj_set_style_radius(m, theme::RADIUS_SM, LV_PART_ITEMS);
|
||||
return m;
|
||||
}
|
||||
|
||||
@@ -115,6 +116,14 @@ static lv_obj_t* textField(lv_obj_t* parent, const char* placeholder = NULL) {
|
||||
return ta;
|
||||
}
|
||||
|
||||
// The primary action of a screen or popup: full amber, dark text (Theme.h);
|
||||
// call after its label is made.
|
||||
static void stylePrimary(lv_obj_t* b) {
|
||||
lv_obj_set_style_bg_color(b, lv_color_hex(theme::ACCENT), 0);
|
||||
for (uint32_t i = 0; i < lv_obj_get_child_count(b); i++)
|
||||
lv_obj_set_style_text_color(lv_obj_get_child(b, i), lv_color_hex(theme::BG), 0);
|
||||
}
|
||||
|
||||
// Unread badge: amber pill with a count, on the right edge of `parent`.
|
||||
static void badge(lv_obj_t* parent, int n, bool overflow) {
|
||||
if (n <= 0) return;
|
||||
@@ -124,7 +133,7 @@ static void badge(lv_obj_t* parent, int n, bool overflow) {
|
||||
lv_obj_set_size(b, LV_SIZE_CONTENT, 20);
|
||||
lv_obj_set_style_bg_color(b, lv_color_hex(theme::ACCENT), 0);
|
||||
lv_obj_set_style_border_width(b, 0, 0);
|
||||
lv_obj_set_style_radius(b, 10, 0);
|
||||
lv_obj_set_style_radius(b, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_pad_hor(b, 7, 0);
|
||||
lv_obj_set_style_pad_ver(b, 0, 0);
|
||||
lv_obj_align(b, LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
||||
@@ -201,15 +210,15 @@ void UITask::begin(DisplayDriver* display_drv, SensorManager* sensors, NodePrefs
|
||||
Serial.println("ui-lvgl: no memory for display buffers");
|
||||
return;
|
||||
}
|
||||
lv_theme_t* th = lv_theme_default_init(lv_display_get_default(), lv_color_hex(theme::ACCENT),
|
||||
lv_color_hex(theme::ACCENT_DIM), true, THEME_FONT_BODY);
|
||||
lv_display_set_theme(lv_display_get_default(), th);
|
||||
theme::setAccent(lvport::loadAccent());
|
||||
theme::install(lv_display_get_default());
|
||||
|
||||
buildStatusBar();
|
||||
applyDisplayPrefs(); // a slider percentage overrides the level main.cpp set
|
||||
showHome();
|
||||
lvport::loadPin(_pin, sizeof(_pin));
|
||||
if (_pin[0]) lockScreen(); // a reboot doesn't get round the PIN
|
||||
showSplash(); // over both; fades out by itself
|
||||
}
|
||||
|
||||
MyMesh::Listener* UITask::meshListener() { return _core; }
|
||||
@@ -279,6 +288,52 @@ void UITask::loop() {
|
||||
(unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM), esp_get_idf_version());
|
||||
}
|
||||
#endif
|
||||
#if defined(UI_PERF_TEST) && defined(ESP32)
|
||||
// -D UI_PERF_TEST: walks through screens by itself and prints how long each
|
||||
// display refresh took during the transition that followed (render+flush).
|
||||
{
|
||||
static uint32_t next = 15000, sum_us = 0, max_us = 0, frames = 0, t0 = 0;
|
||||
static int step = 0;
|
||||
static const char* name = "";
|
||||
static bool hooked = false;
|
||||
if (!hooked) {
|
||||
hooked = true;
|
||||
lv_display_add_event_cb(lv_display_get_default(), [](lv_event_t* e) {
|
||||
if (lv_event_get_code(e) == LV_EVENT_REFR_START) t0 = micros();
|
||||
else { uint32_t d = micros() - t0; sum_us += d; if (d > max_us) max_us = d; frames++; }
|
||||
}, LV_EVENT_REFR_START, NULL);
|
||||
lv_display_add_event_cb(lv_display_get_default(), [](lv_event_t* e) {
|
||||
uint32_t d = micros() - t0; sum_us += d; if (d > max_us) max_us = d; frames++; (void)e;
|
||||
}, LV_EVENT_REFR_READY, NULL);
|
||||
}
|
||||
if (millis() >= next) {
|
||||
if (*name) Serial.printf("PERF %-10s frames %2lu avg %5.1f ms max %5.1f ms\n", name, (unsigned long)frames,
|
||||
frames ? sum_us / 1000.0f / frames : 0.0f, max_us / 1000.0f);
|
||||
if (*name) Serial.printf("PERF flush %5.1f ms per frame\n", frames ? lvport::s_flush_us / 1000.0f / frames : 0.0f);
|
||||
sum_us = max_us = frames = 0;
|
||||
lvport::s_flush_us = 0;
|
||||
lv_display_trigger_activity(NULL); // no sleeping mid-test
|
||||
next = millis() + 1200;
|
||||
if (step == 0) unlockScreen(); // a PIN lock would be drawn over everything
|
||||
if (step % 8 == 0 && step > 0) { // a static full-screen redraw of Home, for the baseline
|
||||
uint32_t fl0 = lvport::s_flush_us, t = micros();
|
||||
for (int i = 0; i < 10; i++) { lv_obj_invalidate(lv_screen_active()); lv_refr_now(NULL); }
|
||||
Serial.printf("PERF full redraw (Home) %5.1f ms, flush %5.1f ms\n", (micros() - t) / 10000.0f,
|
||||
(lvport::s_flush_us - fl0) / 10000.0f);
|
||||
}
|
||||
switch (step++ % 8) {
|
||||
case 0: name = "settings"; showSettings(); break;
|
||||
case 1: name = "back-home"; back(); break;
|
||||
case 2: name = "messages"; showChats(); break;
|
||||
case 3: name = "back-home"; back(); break;
|
||||
case 4: name = "nearby"; showNearby(); break;
|
||||
case 5: name = "back-home"; back(); break;
|
||||
case 6: name = "page-2"; setHomePage(1); break;
|
||||
case 7: name = "page-1"; setHomePage(0); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef PIN_BUZZER
|
||||
_buzzer.loop();
|
||||
if (soundctl::autoTick(_prefs, _buzzer, isClientConnected()) && !_asleep) refreshStatusBar();
|
||||
@@ -484,7 +539,7 @@ void UITask::buildStatusBar() {
|
||||
lv_obj_set_size(_status_icons, 200, LV_PCT(100)); // fixed: packed to its right edge
|
||||
lv_obj_set_flex_flow(_status_icons, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_flex_align(_status_icons, LV_FLEX_ALIGN_END, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_set_style_pad_column(_status_icons, 7, 0);
|
||||
lv_obj_set_style_pad_column(_status_icons, 3, 0);
|
||||
|
||||
_toast = lv_obj_create(lv_layer_top());
|
||||
lv_obj_remove_flag(_toast, LV_OBJ_FLAG_SCROLLABLE);
|
||||
@@ -544,7 +599,7 @@ void UITask::refreshStatusBar() {
|
||||
if (_core->gpsEnabled() || fix) icons[n++] = { LV_SYMBOL_GPS, fix ? theme::OK : theme::TEXT_MUTED };
|
||||
if (_prefs && _prefs->alarm_on) icons[n++] = { LV_SYMBOL_BELL, theme::TEXT_MUTED };
|
||||
#ifdef PIN_BUZZER
|
||||
if (_buzzer.isQuiet()) icons[n++] = { LV_SYMBOL_MUTE, theme::TEXT_MUTED };
|
||||
if (_buzzer.isQuiet()) icons[n++] = { UI_SYMBOL_MUTE, theme::TEXT_MUTED };
|
||||
#endif
|
||||
if (_prefs && _prefs->advert_auto_interval_sec > 0) icons[n++] = { UI_SYMBOL_RADIO, theme::ACCENT };
|
||||
if (_core->trail.isActive()) icons[n++] = { UI_SYMBOL_ROUTE, theme::ACCENT };
|
||||
@@ -558,7 +613,11 @@ void UITask::refreshStatusBar() {
|
||||
if (strcmp(sig, _status_sig) != 0) { // rebuilt only when something changed
|
||||
strcpy(_status_sig, sig);
|
||||
lv_obj_clean(_status_icons);
|
||||
for (int i = n - 1; i >= 0; i--) label(_status_icons, icons[i].sym, THEME_FONT_SMALL, icons[i].col);
|
||||
for (int i = n - 1; i >= 0; i--) { // one cell each: glyphs vary in width, the rhythm doesn't
|
||||
lv_obj_t* l = label(_status_icons, icons[i].sym, THEME_FONT_SMALL, icons[i].col);
|
||||
lv_obj_set_width(l, 16);
|
||||
lv_obj_set_style_text_align(l, LV_TEXT_ALIGN_CENTER, 0);
|
||||
}
|
||||
}
|
||||
lv_obj_align_to(_status_icons, _status_batt, LV_ALIGN_OUT_LEFT_MID, -8, 0);
|
||||
}
|
||||
@@ -581,14 +640,18 @@ bool UITask::ensureGps() {
|
||||
|
||||
// One persistent timer, paused between toasts: hides the toast when it fires.
|
||||
static void toastTimerCb(lv_timer_t* t) {
|
||||
lv_obj_add_flag((lv_obj_t*)lv_timer_get_user_data(t), LV_OBJ_FLAG_HIDDEN);
|
||||
anim::fadeHide((lv_obj_t*)lv_timer_get_user_data(t));
|
||||
lv_timer_pause(t);
|
||||
}
|
||||
|
||||
void UITask::showToast(const char* text, uint32_t ms) {
|
||||
if (!_toast) return;
|
||||
lv_label_set_text(lv_obj_get_child(_toast, 0), text);
|
||||
bool shown = !lv_obj_has_flag(_toast, LV_OBJ_FLAG_HIDDEN);
|
||||
lv_anim_delete(_toast, NULL); // a fade-out in progress
|
||||
lv_obj_set_style_opa(_toast, LV_OPA_COVER, 0);
|
||||
lv_obj_remove_flag(_toast, LV_OBJ_FLAG_HIDDEN);
|
||||
if (!shown) anim::rise(_toast, 12); // a replaced text just changes
|
||||
if (!_toast_timer) _toast_timer = lv_timer_create(toastTimerCb, ms, _toast);
|
||||
lv_timer_set_period(_toast_timer, ms);
|
||||
lv_timer_reset(_toast_timer);
|
||||
@@ -618,7 +681,7 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) {
|
||||
_wifi_scanning = false;
|
||||
_ota_status = _ota_bar = _ota_btn = _ota_btn_lbl = nullptr;
|
||||
for (lv_obj_t*& t : _map_tiles) t = nullptr;
|
||||
lv_obj_t* prev = lv_screen_active();
|
||||
lv_obj_t* prev = _scr;
|
||||
lv_obj_t* scr = lv_obj_create(NULL);
|
||||
styleSurface(scr, theme::BG);
|
||||
lv_obj_remove_flag(scr, LV_OBJ_FLAG_SCROLLABLE);
|
||||
@@ -655,12 +718,35 @@ lv_obj_t* UITask::newScreen(const char* title, bool with_back) {
|
||||
lv_obj_set_style_pad_all(body, theme::PAD, 0);
|
||||
lv_obj_set_style_pad_row(body, theme::GAP, 0);
|
||||
_body = body;
|
||||
// The new screen emerges from the middle (Anim.h); a screen rebuilt in
|
||||
// place (same screen, same title) swaps without motion.
|
||||
bool same = _screen == _shown_screen && strncmp(title ? title : "", _shown_title, sizeof(_shown_title) - 1) == 0;
|
||||
bool backward = _nav_back || screenDepth(_screen) < screenDepth(_shown_screen);
|
||||
_nav_back = false;
|
||||
_shown_screen = _screen;
|
||||
snprintf(_shown_title, sizeof(_shown_title), "%s", title ? title : "");
|
||||
_scr = scr;
|
||||
lv_screen_load(scr);
|
||||
if (prev && prev != scr) lv_obj_delete_async(prev);
|
||||
if (prev) lv_obj_delete_async(prev); // this usually runs from one of its own widgets
|
||||
if (prev && !same && !_asleep) anim::screenIn(scr, body, backward);
|
||||
return body;
|
||||
}
|
||||
|
||||
// How deep a screen sits below Home, for the slide direction.
|
||||
int UITask::screenDepth(Screen s) {
|
||||
switch (s) {
|
||||
case SCR_HOME: return 0;
|
||||
case SCR_CHATS: case SCR_NEARBY: case SCR_MAP: case SCR_SETTINGS: case SCR_FAVS:
|
||||
case SCR_COMPASS: case SCR_CLOCK: case SCR_BOT: case SCR_REPEATER: case SCR_ADMIN_PICK:
|
||||
case SCR_DIAG: return 1;
|
||||
case SCR_THREAD: case SCR_CONTACTS: case SCR_CHANNEL_EDIT: case SCR_NODE:
|
||||
case SCR_SETTINGS_NAV: case SCR_ADMIN: return 2;
|
||||
default: return 3; // pages under a settings page
|
||||
}
|
||||
}
|
||||
|
||||
void UITask::back() {
|
||||
_nav_back = true; // the screen shown from here slides back
|
||||
switch (_screen) {
|
||||
case SCR_THREAD: if (_nav_overlay) navClosePopup(); else showChats(); break;
|
||||
case SCR_CONTACTS: showChats(); break;
|
||||
@@ -721,9 +807,10 @@ void UITask::back() {
|
||||
else if (!_map_nav) showNearby(); // the Nodes map opens from Nearby
|
||||
else showHome();
|
||||
break;
|
||||
case SCR_WIFI: if (_wifi_from_map) showMap(); else showSettings(); break;
|
||||
case SCR_WIFI: showSettings(); break;
|
||||
default: break;
|
||||
}
|
||||
_nav_back = false; // only closed a popup
|
||||
}
|
||||
|
||||
// ── Home ──────────────────────────────────────────────────────────────────────
|
||||
@@ -868,6 +955,7 @@ void UITask::homeSwipePoll() {
|
||||
void UITask::setHomePage(int page) {
|
||||
if (_screen != SCR_HOME || !home::s_row) return; // s_row went with an older screen
|
||||
if (page < 0 || page >= home::PAGES) return;
|
||||
int from = home::s_page;
|
||||
home::s_page = page;
|
||||
lv_obj_clean(home::s_row);
|
||||
_home_unread = nullptr;
|
||||
@@ -884,6 +972,7 @@ void UITask::setHomePage(int page) {
|
||||
}
|
||||
for (int i = 0; home::s_dots && i < (int)lv_obj_get_child_count(home::s_dots); i++)
|
||||
lv_obj_set_style_bg_color(lv_obj_get_child(home::s_dots, i), lv_color_hex(i == page ? theme::ACCENT : theme::SURFACE_2), 0);
|
||||
if (page != from) anim::slideIn(home::s_row, page > from ? 48 : -48);
|
||||
refreshHome();
|
||||
}
|
||||
|
||||
@@ -979,7 +1068,7 @@ static void sectionWithFilter(lv_obj_t* parent, const char* text, bool fav_only,
|
||||
lv_obj_set_size(b, LV_SIZE_CONTENT, 24);
|
||||
lv_obj_set_style_pad_hor(b, 10, 0);
|
||||
lv_obj_set_style_pad_ver(b, 0, 0);
|
||||
lv_obj_set_style_radius(b, 12, 0);
|
||||
lv_obj_set_style_radius(b, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_shadow_width(b, 0, 0);
|
||||
lv_obj_set_style_bg_color(b, lv_color_hex(fav_only ? theme::ACCENT_DIM : theme::SURFACE), 0);
|
||||
lv_obj_align(b, LV_ALIGN_RIGHT_MID, -2, 0);
|
||||
@@ -1015,7 +1104,7 @@ void UITask::buildChats() {
|
||||
ch_rows++;
|
||||
char title[48], sub[64] = "";
|
||||
snprintf(title, sizeof(title), "%s%s%s", fav ? UI_SYMBOL_STAR " " : "", ch.name,
|
||||
chanctl::notif(_prefs, i) == chanctl::NOTIF_MUTED ? " " LV_SYMBOL_MUTE : "");
|
||||
chanctl::notif(_prefs, i) == chanctl::NOTIF_MUTED ? " " UI_SYMBOL_MUTE : "");
|
||||
int n = _core->history.histCountForChannel(i);
|
||||
if (n > 0) {
|
||||
const ChHistEntry& e = _core->history.chAtPos(_core->history.histEntryForChannel(i, 0));
|
||||
@@ -1052,7 +1141,7 @@ void UITask::buildChats() {
|
||||
char name[48];
|
||||
contactName(e.prefix, name, sizeof(name));
|
||||
if (known && contactctl::favourite(c)) { char t[48]; snprintf(t, sizeof(t), UI_SYMBOL_STAR " %s", name); strcpy(name, t); }
|
||||
if (known && contactctl::notif(_prefs, c.id.pub_key) == contactctl::NOTIF_MUTED) strncat(name, " " LV_SYMBOL_MUTE, sizeof(name) - strlen(name) - 1);
|
||||
if (known && contactctl::notif(_prefs, c.id.pub_key) == contactctl::NOTIF_MUTED) strncat(name, " " UI_SYMBOL_MUTE, sizeof(name) - strlen(name) - 1);
|
||||
char sub[64];
|
||||
snprintf(sub, sizeof(sub), "%s%s", e.outgoing ? "Me: " : "", e.text);
|
||||
lv_obj_t* row = listRow(body, name, sub, onOpenDMRow, (void*)(uintptr_t)rows);
|
||||
@@ -1101,6 +1190,7 @@ void UITask::buildChats() {
|
||||
lv_obj_set_style_shadow_width(add, 0, 0);
|
||||
lv_obj_add_event_cb(add, onNewChat, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_center(label(add, LV_SYMBOL_PLUS " New message", THEME_FONT_BODY, theme::TEXT));
|
||||
stylePrimary(add);
|
||||
}
|
||||
|
||||
// ── Contact picker (start a DM) ───────────────────────────────────────────────
|
||||
@@ -1192,12 +1282,12 @@ void UITask::buildNearby() {
|
||||
lv_obj_set_height(c, 26);
|
||||
lv_obj_set_flex_grow(c, 1);
|
||||
lv_obj_set_style_pad_all(c, 0, 0);
|
||||
lv_obj_set_style_radius(c, 13, 0);
|
||||
lv_obj_set_style_radius(c, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_shadow_width(c, 0, 0);
|
||||
bool on = f == _nearby->filter();
|
||||
lv_obj_set_style_bg_color(c, lv_color_hex(on ? theme::ACCENT : theme::SURFACE), 0);
|
||||
lv_obj_set_style_bg_color(c, lv_color_hex(on ? theme::ACCENT_DIM : theme::SURFACE), 0); // selected (Theme.h)
|
||||
lv_obj_add_event_cb(c, onNearbyChip, LV_EVENT_CLICKED, (void*)(uintptr_t)f);
|
||||
lv_obj_center(label(c, NearbyModel::filterLabel(f), THEME_FONT_SMALL, on ? theme::BG : theme::TEXT));
|
||||
lv_obj_center(label(c, NearbyModel::filterLabel(f), THEME_FONT_SMALL, theme::TEXT));
|
||||
}
|
||||
|
||||
_nearby_status = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
@@ -1238,7 +1328,7 @@ void UITask::startNearbyScan() {
|
||||
// Dimmed full-screen overlay (swallows taps) holding a panel with the results.
|
||||
// A child of the current screen, so it goes away with it.
|
||||
void UITask::showScanPopup() {
|
||||
_scan_overlay = lv_obj_create(lv_screen_active());
|
||||
_scan_overlay = lv_obj_create(screen());
|
||||
lv_obj_remove_style_all(_scan_overlay);
|
||||
lv_obj_set_size(_scan_overlay, LV_PCT(100), LV_PCT(100));
|
||||
lv_obj_set_style_bg_color(_scan_overlay, lv_color_hex(0x000000), 0);
|
||||
@@ -1247,6 +1337,7 @@ void UITask::showScanPopup() {
|
||||
lv_obj_remove_flag(_scan_overlay, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
lv_obj_t* panel = lv_obj_create(_scan_overlay);
|
||||
anim::popup(_scan_overlay);
|
||||
lv_obj_set_size(panel, lv_display_get_horizontal_resolution(NULL) - 16,
|
||||
lv_display_get_vertical_resolution(NULL) - theme::STATUS_H - 12);
|
||||
lv_obj_set_pos(panel, 8, theme::STATUS_H + 6);
|
||||
@@ -1757,7 +1848,7 @@ void UITask::buildThread() {
|
||||
if (_compose_ta && _share_text[0]) { // shareToMessage(): the text waits in the field
|
||||
lv_textarea_set_text(_compose_ta, _share_text);
|
||||
_share_text[0] = '\0';
|
||||
lv_obj_update_layout(lv_screen_active());
|
||||
lv_obj_update_layout(screen());
|
||||
setKeyboardVisible(true);
|
||||
}
|
||||
}
|
||||
@@ -1814,7 +1905,7 @@ static void msgLocButton(lv_obj_t* parent, const char* text, int idx, bool save,
|
||||
lv_obj_set_size(b, LV_SIZE_CONTENT, 28);
|
||||
lv_obj_set_style_pad_hor(b, 10, 0);
|
||||
lv_obj_set_style_pad_ver(b, 0, 0);
|
||||
lv_obj_set_style_radius(b, 14, 0);
|
||||
lv_obj_set_style_radius(b, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_shadow_width(b, 0, 0);
|
||||
lv_obj_set_style_bg_color(b, lv_color_hex(accent ? theme::ACCENT : theme::SURFACE_2), 0);
|
||||
lv_obj_add_event_cb(b, onMsgLoc, LV_EVENT_CLICKED, (void*)(uintptr_t)((idx << 1) | (save ? 1 : 0)));
|
||||
@@ -1891,12 +1982,15 @@ static void bubble(lv_obj_t* list, const char* from, const char* text, bool own,
|
||||
msgLocButton(acts, UI_SYMBOL_FLAG " Save", loc_idx, true, false);
|
||||
}
|
||||
|
||||
if (!meta_in_header) {
|
||||
lv_obj_t* m = label(b, meta, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
if (status) {
|
||||
lv_label_set_text_fmt(m, "%s %s", meta, status);
|
||||
lv_obj_set_style_text_color(m, lv_color_hex(status_col), 0);
|
||||
}
|
||||
if (!meta_in_header) { // the age, then the delivery mark in its colour (as L1: ✓ 3)
|
||||
lv_obj_t* line = lv_obj_create(b);
|
||||
lv_obj_remove_style_all(line);
|
||||
lv_obj_remove_flag(line, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_set_size(line, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||
lv_obj_set_flex_flow(line, LV_FLEX_FLOW_ROW);
|
||||
lv_obj_set_style_pad_column(line, 6, 0);
|
||||
label(line, meta, THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
if (status && status[0]) label(line, status, THEME_FONT_SMALL, status_col);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1955,9 +2049,16 @@ void UITask::refreshThread() {
|
||||
body = sep + 2;
|
||||
}
|
||||
bool own = strcmp(from, "Me") == 0;
|
||||
// Own posts: a check once a repeater echoed it, with how many distinct
|
||||
// repeaters did (markChannelRelayed); nothing before -- no echo is normal.
|
||||
const char* st = NULL; uint32_t col = theme::TEXT_MUTED;
|
||||
if (own && e.relay_status == ACK_OK) { st = LV_SYMBOL_OK " relayed"; col = theme::OK; }
|
||||
else if (own && e.relay_status == ACK_PENDING) st = "sent";
|
||||
char relays[16];
|
||||
if (own && e.relay_status == ACK_OK) {
|
||||
int nrel = e.path_len & 63;
|
||||
if (nrel > 0) snprintf(relays, sizeof(relays), LV_SYMBOL_OK " %d", nrel);
|
||||
else snprintf(relays, sizeof(relays), LV_SYMBOL_OK);
|
||||
st = relays; col = theme::OK;
|
||||
} else if (own) st = "";
|
||||
int loc = own ? -1 : noteMsgLocation(body, from);
|
||||
bubble(_thread_list, own ? NULL : from, body, own, e.timestamp, st, col,
|
||||
loc, noteMsgMeta(pos, true, own, loc, own ? "" : from));
|
||||
@@ -1973,10 +2074,10 @@ void UITask::refreshThread() {
|
||||
const char* st = NULL; uint32_t col = theme::TEXT_MUTED;
|
||||
if (e.outgoing) {
|
||||
switch (h.dmEffectiveStatus(e)) {
|
||||
case ACK_OK: st = LV_SYMBOL_OK " delivered"; col = theme::OK; break;
|
||||
case ACK_FAIL: st = LV_SYMBOL_CLOSE " not delivered"; col = theme::FAIL; break;
|
||||
case ACK_PENDING: st = "sending..."; break;
|
||||
default: st = "sent"; break;
|
||||
case ACK_OK: st = LV_SYMBOL_OK; col = theme::OK; break;
|
||||
case ACK_FAIL: st = LV_SYMBOL_CLOSE; col = theme::FAIL; break;
|
||||
case ACK_PENDING: st = "..."; break;
|
||||
default: st = ""; break;
|
||||
}
|
||||
}
|
||||
// A room post is filed "Author: text": shown under its author.
|
||||
@@ -2169,6 +2270,8 @@ void UITask::buildSchemaSettings() {
|
||||
sectionTitle(body, "SECURITY");
|
||||
listRow(body, LV_SYMBOL_EYE_CLOSE " Screen PIN", _pin[0] ? "On - asked when the screen wakes" : "Off",
|
||||
onPinSetup, NULL);
|
||||
sectionTitle(body, "LOOK");
|
||||
accentRow(body); // DeviceScreen.h
|
||||
}
|
||||
if (_settings_page == settings::PG_SOUND) buildSoundRows(body, false); // the melodies
|
||||
if (_settings_page == settings::PG_MESSAGES) {
|
||||
@@ -2357,3 +2460,4 @@ void UITask::setKeyboardAlphabets(int main_idx, int alt_sel) {
|
||||
#include "SoundScreen.h"
|
||||
#include "QuickScreen.h"
|
||||
#include "OtaScreen.h"
|
||||
#include "Splash.h"
|
||||
|
||||
@@ -89,7 +89,7 @@ public:
|
||||
void navCoordsDone(bool ok);
|
||||
void shareToMessage(const char* text);
|
||||
void messageLocationAction(int idx, bool save);
|
||||
void showWifi(bool from_map);
|
||||
void showWifi();
|
||||
void wifiSetAllowed(bool on);
|
||||
void wifiScan();
|
||||
void wifiPick(int idx);
|
||||
@@ -214,6 +214,8 @@ public:
|
||||
void pinSetupKey(const char* key);
|
||||
void pinRemove();
|
||||
void pinRowRefresh();
|
||||
void accentRow(lv_obj_t* body); // Settings > Display & power > Accent colour
|
||||
void setAccent(int idx);
|
||||
void showFavourites();
|
||||
void showDiag();
|
||||
void diagTab(int tab);
|
||||
@@ -239,6 +241,10 @@ private:
|
||||
void buildStatusBar();
|
||||
void refreshStatusBar();
|
||||
lv_obj_t* newScreen(const char* title, bool with_back);
|
||||
void showSplash(); // Splash.h
|
||||
// The screen being shown: newScreen()'s, already while it slides in (popups
|
||||
// and keyboards go on it, not on the one sliding out).
|
||||
lv_obj_t* screen() const { return _scr ? _scr : lv_screen_active(); }
|
||||
void buildHome();
|
||||
void refreshHome();
|
||||
void homeSwipePoll();
|
||||
@@ -355,6 +361,12 @@ private:
|
||||
UiCore* _core = nullptr;
|
||||
|
||||
Screen _screen = SCR_HOME;
|
||||
// What newScreen() last loaded, for the slide direction (Anim.h)
|
||||
Screen _shown_screen = SCR_HOME;
|
||||
char _shown_title[32] = "";
|
||||
bool _nav_back = false; // set by back() around the screen it shows
|
||||
lv_obj_t* _scr = nullptr; // the screen newScreen() built (active once its slide starts)
|
||||
static int screenDepth(Screen s);
|
||||
bool _asleep = false;
|
||||
uint32_t _next_status_ms = 0;
|
||||
uint32_t _next_thread_check_ms = 0;
|
||||
@@ -473,7 +485,6 @@ private:
|
||||
char _share_text[96] = ""; // waiting for a conversation to be picked (shareToMessage)
|
||||
|
||||
// WiFi settings (SCR_WIFI)
|
||||
bool _wifi_from_map = false;
|
||||
bool _wifi_scanning = false;
|
||||
lv_obj_t* _wifi_ssid = nullptr;
|
||||
lv_obj_t* _wifi_pass = nullptr;
|
||||
|
||||
@@ -28,8 +28,7 @@ static lv_obj_t* wifiField(lv_obj_t* parent, const char* placeholder, bool passw
|
||||
return ta;
|
||||
}
|
||||
|
||||
void UITask::showWifi(bool from_map) {
|
||||
_wifi_from_map = from_map;
|
||||
void UITask::showWifi() {
|
||||
_screen = SCR_WIFI;
|
||||
buildWifi();
|
||||
}
|
||||
@@ -75,6 +74,7 @@ void UITask::buildWifi() {
|
||||
lv_obj_set_style_bg_color(save, lv_color_hex(theme::ACCENT_DIM), 0);
|
||||
lv_obj_add_event_cb(save, onWifiSave, LV_EVENT_CLICKED, NULL);
|
||||
lv_obj_center(label(save, LV_SYMBOL_OK " Save", THEME_FONT_BODY, theme::TEXT));
|
||||
stylePrimary(save);
|
||||
_wifi_status = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
||||
|
||||
char ssid[33], pass[65];
|
||||
@@ -84,7 +84,7 @@ void UITask::buildWifi() {
|
||||
|
||||
// Keyboard over the bottom of the screen; the body shrinks above it while
|
||||
// it is up, so the field being edited stays visible.
|
||||
_wifi_kb = kb::create(lv_screen_active(), _prefs);
|
||||
_wifi_kb = kb::create(screen(), _prefs);
|
||||
lv_obj_set_size(_wifi_kb, LV_PCT(100), 124);
|
||||
lv_obj_align(_wifi_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
||||
lv_obj_add_event_cb(_wifi_kb, onWifiKb, LV_EVENT_READY, NULL);
|
||||
@@ -102,7 +102,7 @@ void UITask::wifiEdit(lv_obj_t* ta) {
|
||||
lv_obj_remove_flag(_wifi_kb, LV_OBJ_FLAG_HIDDEN);
|
||||
if (_body) lv_obj_set_height(_body, lv_obj_get_height(_body) - lv_obj_get_height(_wifi_kb));
|
||||
}
|
||||
lv_obj_update_layout(lv_screen_active());
|
||||
lv_obj_update_layout(screen());
|
||||
lv_obj_scroll_to_view(ta, LV_ANIM_OFF);
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@ static void wifiRow(lv_obj_t* body) {
|
||||
lv_obj_t* row = lv_obj_get_parent(sw);
|
||||
lv_obj_add_flag(row, LV_OBJ_FLAG_CLICKABLE);
|
||||
lv_obj_add_event_cb(row, [](lv_event_t* e) {
|
||||
if (lv_event_get_target(e) == lv_event_get_current_target(e)) s_ui->showWifi(false); // not the switch
|
||||
if (lv_event_get_target(e) == lv_event_get_current_target(e)) s_ui->showWifi(); // not the switch
|
||||
}, LV_EVENT_CLICKED, NULL);
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ void UITask::pollWifiScan() {
|
||||
lv_obj_set_style_pad_hor(b, 10, 0);
|
||||
lv_obj_set_style_pad_ver(b, 0, 0);
|
||||
lv_obj_set_style_shadow_width(b, 0, 0);
|
||||
lv_obj_set_style_radius(b, 15, 0);
|
||||
lv_obj_set_style_radius(b, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_bg_color(b, lv_color_hex(theme::SURFACE), 0);
|
||||
lv_obj_add_event_cb(b, onWifiPick, LV_EVENT_CLICKED, (void*)(uintptr_t)i);
|
||||
lv_obj_center(label(b, s_wifi_names[i], THEME_FONT_SMALL, theme::TEXT));
|
||||
@@ -192,6 +192,5 @@ void UITask::wifiSave() {
|
||||
lvport::saveWifi(ssid, pass);
|
||||
wifiKeyboardHide();
|
||||
showToast("WiFi saved");
|
||||
if (_wifi_from_map) { showMap(); mapDownloadPopup(); }
|
||||
else lv_label_set_text(_wifi_status, "Saved.");
|
||||
lv_label_set_text(_wifi_status, "Saved.");
|
||||
}
|
||||
|
||||
@@ -30,12 +30,13 @@ TEXT="$TEXT,0x2010-0x2027,0x2030,0x2039-0x203A,0x20A4-0x20BF,0x2116,0x2122"
|
||||
# 62073 = map, 61632 = users (home tiles), 61774 = compass (Navigate),
|
||||
# 61476 = flag (waypoint), 62405 = map marker (mark here), 61463 = clock,
|
||||
# 62194 = stopwatch, 62745 = broadcast tower (radio), 61568 = bar chart (diagnostics),
|
||||
# 61572 = key (admin), 62679 = route (trail in the status bar)
|
||||
SYMS="61572,62679,61568,62745,61463,62194,61612,61445,62073,61632,61774,61476,62405,61441,61448,61451,61452,61453,61457,61459,61461,61465,61468,61473,61478,61479,61480,61502,61507,61512,61515,61516,61517,61521,61522,61523,61524,61543,61544,61550,61552,61553,61556,61559,61560,61561,61563,61587,61589,61636,61637,61639,61641,61664,61671,61674,61683,61724,61732,61787,61931,62016,62017,62018,62019,62020,62087,62099,62212,62189,62810,63426,63650"
|
||||
# 61572 = key (admin), 62679 = route (trail in the status bar),
|
||||
# 63145 = volume-mute (muted, in the status bar)
|
||||
SYMS="63145,61572,62679,61568,62745,61463,62194,61612,61445,62073,61632,61774,61476,62405,61441,61448,61451,61452,61453,61457,61459,61461,61465,61468,61473,61478,61479,61480,61502,61507,61512,61515,61516,61517,61521,61522,61523,61524,61543,61544,61550,61552,61553,61556,61559,61560,61561,61563,61587,61589,61636,61637,61639,61641,61664,61671,61674,61683,61724,61732,61787,61931,62016,62017,62018,62019,62020,62087,62099,62212,62189,62810,63426,63650"
|
||||
|
||||
CONV="npx -y lv_font_conv@1.5.3"
|
||||
gen() { # size weight ranges name
|
||||
$CONV --bpp 4 --size "$1" --force-fast-kern-format \
|
||||
$CONV --bpp 4 --size "$1" --no-compress --force-fast-kern-format \
|
||||
--font "$CACHE/NotoSans-$2.ttf" -r "$3" \
|
||||
--font "$CACHE/fa.woff" -r "$SYMS" \
|
||||
--format lvgl --lv-include lvgl.h --lv-font-name "$4" -o "$HERE/$4.c"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+15412
-11346
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,7 @@
|
||||
LV_FONT_DECLARE(ui_font_16) LV_FONT_DECLARE(ui_font_20) \
|
||||
LV_FONT_DECLARE(ui_font_40)
|
||||
#define LV_FONT_DEFAULT &ui_font_14
|
||||
#define LV_USE_FONT_COMPRESSED 1 // generated fonts are RLE-compressed (without this every glyph draws as a box)
|
||||
#define LV_USE_FONT_COMPRESSED 0 // fonts are generated uncompressed: no per-glyph decompression on every draw
|
||||
|
||||
// PNG decoder for raster map tiles (map/TileProvider.h calls lodepng directly).
|
||||
#define LV_USE_LODEPNG 1
|
||||
|
||||
Reference in New Issue
Block a user