feat(ui-lvgl): @nick mentions highlighted, colour emoji and flags

Mentions: "@[nick]" in message text becomes "@nick" in the accent colour
(a span group); a mention of us is underlined and its bubble outlined.
Chat previews and the message popup's quote show "@nick" as well.

Emoji: every single-codepoint Twemoji and every flag, 16 px RGB565A8 in
flash, as the fallback font of each text size (fonts/ui_emoji.c, data
generated by fonts/emoji.py). Joiners, selectors and skin tones take no
space; a regional-indicator pair is swapped for the flag's private
codepoint before text reaches a label. Twemoji credit in Settings > About.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-26 23:41:12 +02:00
co-authored by Claude Opus 5.5
parent 708fde7225
commit f5206f10b4
13 changed files with 54002 additions and 18 deletions
@@ -303,7 +303,10 @@ void UITask::messageMenu(int idx) {
}
// The message itself, quoted short, and when.
lv_obj_t* q = label(panel, body, THEME_FONT_SMALL, theme::TEXT);
char quote[MAX_TEXT_LEN + 1];
snprintf(quote, sizeof(quote), "%s", body);
plainMentions(quote);
lv_obj_t* q = label(panel, quote, 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
@@ -883,4 +883,6 @@ void UITask::mapDownloadTick() {
#if defined(SIM_PLATFORM) && defined(__EMSCRIPTEN__)
// Sim page / tests: a saved WiFi network without going through Settings > WiFi.
extern "C" EMSCRIPTEN_KEEPALIVE void sim_wifi_save(const char* ssid, const char* pass) { lvport::saveWifi(ssid, pass); }
// ...and this device's name (a test mentions it).
extern "C" EMSCRIPTEN_KEEPALIVE const char* sim_node_name() { return the_mesh.getNodeName(); }
#endif
+118 -6
View File
@@ -6,6 +6,43 @@
#include <esp_heap_caps.h>
#endif
#include <new>
#include <stdarg.h>
// Flags: LVGL draws text one codepoint at a time, so a flag (a pair of regional
// indicator letters) would show as two letter tiles. Every label and span of
// this UI (all of it is this one file and the headers it includes) goes through
// these, which swap each pair for the codepoint fonts/ui_emoji.c keeps the
// flag's image under. Display only: what's stored and sent stays as it was.
extern "C" char* ui_emoji_flags(const char* s);
static void ui_label_set_text(lv_obj_t* o, const char* t) {
char* f = t ? ui_emoji_flags(t) : nullptr; // t == NULL: LVGL's "redraw the current text"
lv_label_set_text(o, f ? f : t);
lv_free(f);
}
static void ui_label_set_text_fmt(lv_obj_t* o, const char* fmt, ...) __attribute__((format(printf, 2, 3)));
static void ui_label_set_text_fmt(lv_obj_t* o, const char* fmt, ...) {
va_list ap;
va_start(ap, fmt);
char buf[160];
int n = vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
if (n < (int)sizeof(buf)) { ui_label_set_text(o, buf); return; }
char* big = (char*)lv_malloc(n + 1);
if (!big) return;
va_start(ap, fmt);
vsnprintf(big, n + 1, fmt, ap);
va_end(ap);
ui_label_set_text(o, big);
lv_free(big);
}
static void ui_span_set_text(lv_span_t* s, const char* t) {
char* f = ui_emoji_flags(t);
lv_span_set_text(s, f ? f : t);
lv_free(f);
}
#define lv_label_set_text ui_label_set_text
#define lv_label_set_text_fmt ui_label_set_text_fmt
#define lv_span_set_text ui_span_set_text
// A zeroed buffer of `n` T in PSRAM when the board has it: internal RAM is what
// BLE, WiFi and TLS need. Allocated once (at startup or first use), never freed.
@@ -135,6 +172,23 @@ static void stylePrimary(lv_obj_t* b) {
}
// Unread badge: amber pill with a count, on the right edge of `parent`.
// "@[nick]" -> "@nick" in place, for one-line text (previews, quotes) where
// the bubble's highlight doesn't reach.
static void plainMentions(char* t) {
char* w = t;
for (const char* r = t; *r;) {
const char* close = r[0] == '@' && r[1] == '[' ? strchr(r + 2, ']') : nullptr;
if (close && close - r <= 34) {
*w++ = '@';
for (const char* q = r + 2; q < close;) *w++ = *q++;
r = close + 1;
} else {
*w++ = *r++;
}
}
*w = '\0';
}
static void badge(lv_obj_t* parent, int n, bool overflow) {
if (n <= 0) return;
lv_obj_t* b = lv_obj_create(parent);
@@ -1179,6 +1233,7 @@ void UITask::buildChats() {
if (n > 0) {
const ChHistEntry& e = _core->history.chAtPos(_core->history.histEntryForChannel(i, 0));
snprintf(sub, sizeof(sub), "%s", e.text);
plainMentions(sub);
}
lv_obj_t* row = listRow(body, title, sub[0] ? sub : NULL, onOpenChannel, (void*)(uintptr_t)i);
lv_obj_add_event_cb(row, onChanRowHold, LV_EVENT_LONG_PRESSED, (void*)(uintptr_t)i);
@@ -1214,6 +1269,7 @@ void UITask::buildChats() {
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);
plainMentions(sub);
lv_obj_t* row = listRow(body, name, sub, onOpenDMRow, (void*)(uintptr_t)rows);
if (known) lv_obj_add_event_cb(row, onDMRowHold, LV_EVENT_LONG_PRESSED, (void*)(uintptr_t)rows);
badge(row, _core->dmUnread(e.prefix), _core->dmUnreadOverflow(e.prefix));
@@ -1990,6 +2046,59 @@ static void msgLocButton(lv_obj_t* parent, const char* text, int idx, bool save,
lv_obj_center(label(b, text, THEME_FONT_SMALL, accent ? theme::BG : theme::TEXT));
}
// A message's text, wrapped at `max_w` and shrunk to fit. "@[nick]" mentions
// (how a reply names who it answers) show as "@nick" in the accent -- in the
// text colour on our own amber bubbles -- and underlined when the nick is
// ours (*mentions_me set). Plain text stays a label.
static lv_obj_t* msgText(lv_obj_t* parent, const char* text, bool own, int max_w, bool* mentions_me) {
*mentions_me = false;
if (!strstr(text, "@[")) {
lv_obj_t* t = label(parent, text, THEME_FONT_BODY, theme::TEXT);
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
lv_obj_set_style_max_width(t, max_w, 0);
lv_obj_set_width(t, LV_SIZE_CONTENT);
return t;
}
lv_obj_t* sg = lv_spangroup_create(parent);
lv_obj_remove_flag(sg, LV_OBJ_FLAG_CLICKABLE); // a hold still reaches the bubble (its menu)
lv_obj_set_style_text_font(sg, THEME_FONT_BODY, 0);
lv_obj_set_style_text_color(sg, lv_color_hex(theme::TEXT), 0);
lv_spangroup_set_mode(sg, LV_SPAN_MODE_BREAK);
const char* me = the_mesh.getNodeName();
char part[MAX_TEXT_LEN + 1];
const char* p = text;
while (*p) {
const char* at = strstr(p, "@[");
const char* close = at ? strchr(at + 2, ']') : nullptr;
if (at && (!close || close - at > 34)) close = nullptr; // not a mention: a nick is <= 31 chars
const char* end = at && close ? at : p + strlen(p);
if (at && !close) end = at + 2; // "@[" alone: plain, carry on after it
if (end > p) {
size_t n = end - p < (ptrdiff_t)sizeof(part) ? end - p : sizeof(part) - 1;
memcpy(part, p, n);
part[n] = '\0';
lv_span_set_text(lv_spangroup_new_span(sg), part);
}
if (!at || !close) { p = end; continue; }
size_t n = close - (at + 2);
part[0] = '@';
memcpy(part + 1, at + 2, n);
part[n + 1] = '\0';
bool mine = strlen(me) == n && strncasecmp(me, at + 2, n) == 0;
lv_span_t* sp = lv_spangroup_new_span(sg);
lv_span_set_text(sp, part);
lv_style_t* st = lv_span_get_style(sp);
lv_style_set_text_color(st, lv_color_hex(own ? theme::TEXT : theme::ACCENT));
if (mine) { lv_style_set_text_decor(st, LV_TEXT_DECOR_UNDERLINE); *mentions_me = true; }
p = close + 1;
}
lv_spangroup_refr_mode(sg);
uint32_t w = lv_spangroup_get_expand_width(sg, 0);
lv_obj_set_width(sg, w > (uint32_t)max_w ? max_w : (int32_t)w + 1);
lv_obj_set_height(sg, LV_SIZE_CONTENT);
return sg;
}
// One message bubble. Own messages right-aligned in amber, others left.
// loc_idx >= 0: the text carries a position (s_msg_locs[loc_idx]).
static void bubble(lv_obj_t* list, const char* from, const char* text, bool own,
@@ -2042,10 +2151,12 @@ static void bubble(lv_obj_t* list, const char* from, const char* text, bool own,
label(hdr, from, THEME_FONT_SMALL, theme::ACCENT); // names are <= 31 chars: fits the bubble
if (meta_in_header) label(hdr, meta, THEME_FONT_SMALL, theme::TEXT_MUTED);
}
lv_obj_t* t = label(b, text, THEME_FONT_BODY, theme::TEXT);
lv_label_set_long_mode(t, LV_LABEL_LONG_WRAP);
lv_obj_set_style_max_width(t, 238, 0);
lv_obj_set_width(t, LV_SIZE_CONTENT);
bool mentions_me;
msgText(b, text, own, 238, &mentions_me);
if (mentions_me && !own) { // someone answering us: the bubble outlined
lv_obj_set_style_border_width(b, 1, 0);
lv_obj_set_style_border_color(b, lv_color_hex(theme::ACCENT), 0);
}
if (loc_idx >= 0) { // position: navigate there / keep it as a waypoint
lv_obj_t* acts = lv_obj_create(b);
@@ -2605,9 +2716,10 @@ void UITask::buildSettings() {
}
sectionTitle(body, "ABOUT");
char about[200], built[24] = "";
char about[300], built[24] = "";
if (!strstr(FIRMWARE_VERSION, FIRMWARE_BUILD_DATE)) snprintf(built, sizeof(built), " (%s)", FIRMWARE_BUILD_DATE);
snprintf(about, sizeof(about), "%s\nFirmware %s%s\n\nMap data: %s", the_mesh.getNodeName(), FIRMWARE_VERSION, built,
snprintf(about, sizeof(about), "%s\nFirmware %s%s\n\nMap data: %s\nEmoji: Twemoji \xC2\xA9 Twitter, Inc. and contributors (CC\xE2\x80\x91" "BY 4.0)",
the_mesh.getNodeName(), FIRMWARE_VERSION, built,
(lvport::mountStorage() && mapview::s_provider->available()) ? mapview::s_provider->attribution()
: "\xC2\xA9 OpenStreetMap contributors");
lv_obj_t* a = label(body, about, THEME_FONT_SMALL, theme::TEXT);
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Builds ui_emoji_data.c: every single-codepoint Twemoji and every flag, scaled to PX x PX,
as LVGL RGB565A8 images (an RGB565 plane, then an alpha plane).
ui_emoji.c turns the table into fonts the text fonts fall back to, so an emoji
in a message, a name or a preview draws in colour. Multi-codepoint sequences
(ZWJ families, skin tones) have no shaping in LVGL: they draw as their parts,
a toned hand untoned. Flags are the exception: each is stored under a private
codepoint, FLAG_BASE + letter1 * 26 + letter2, and UITask.cpp swaps a pair of
regional indicator letters for it before the text reaches a label.
Twemoji graphics: (c) Twitter / X and other contributors, CC-BY 4.0
(https://github.com/jdecked/twemoji).
Usage: emoji.py <twemoji assets/72x72 dir> <out.c> (needs Pillow)
Called by generate.sh.
"""
import os
import sys
from PIL import Image
PX = 16
FLAG_BASE = 0xF0000 # Supplementary Private Use Area-A
RI_A = 0x1F1E6 # regional indicator symbol letter A
def codepoints(src):
"""(codepoint, file) of every glyph, sorted."""
cps = []
for f in os.listdir(src):
name, ext = os.path.splitext(f)
if ext != ".png":
continue
parts = [int(x, 16) for x in name.split("-")]
if len(parts) == 2 and all(RI_A <= p < RI_A + 26 for p in parts): # a flag
cps.append((FLAG_BASE + (parts[0] - RI_A) * 26 + parts[1] - RI_A, f))
continue
if len(parts) > 1:
continue
cp = parts[0]
# below U+2000 the text fonts already have it (digits, (c), (r)); the
# skin tone swatches only ever follow another emoji: ui_emoji.c drops them
if cp < 0x2000 or 0x1F3FB <= cp <= 0x1F3FF:
continue
cps.append((cp, f))
return sorted(cps)
def main(src, out):
glyphs = codepoints(src)
cps = [cp for cp, _ in glyphs]
data = bytearray()
for _, f in glyphs:
im = Image.open(os.path.join(src, f)).convert("RGBA")
im = im.resize((PX, PX), Image.LANCZOS) # Pillow premultiplies RGBA while resampling
rgb, alpha = bytearray(), bytearray()
raw = im.tobytes()
for i in range(0, len(raw), 4):
r, g, b, a = raw[i:i + 4]
v = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)
rgb += bytes((v & 0xFF, v >> 8))
alpha.append(a)
data += rgb + alpha
n = len(cps)
size = PX * PX * 3
with open(out, "w") as o:
o.write("/* Generated by emoji.py from Twemoji 15.1 (CC-BY 4.0) -- do not edit. */\n")
o.write('#include "lvgl.h"\n\n')
o.write("const uint16_t ui_emoji_count = %d;\n\n" % n)
o.write("const uint32_t ui_emoji_cps[%d] = {\n" % n)
for i in range(0, n, 8):
o.write(" " + ", ".join("0x%X" % c for c in cps[i:i + 8]) + ",\n")
o.write("};\n\n")
o.write("static const uint8_t px[%d] = {\n" % len(data))
for i in range(0, len(data), 24):
o.write(" " + ",".join("0x%02X" % b for b in data[i:i + 24]) + ",\n")
o.write("};\n\n")
o.write("#define E(i) { .header = { .magic = LV_IMAGE_HEADER_MAGIC, .cf = LV_COLOR_FORMAT_RGB565A8, "
".w = %d, .h = %d, .stride = %d }, .data_size = %d, .data = px + (i) * %d }\n"
% (PX, PX, PX * 2, size, size))
o.write("const lv_image_dsc_t ui_emoji_img[%d] = {\n" % n)
for i in range(0, n, 10):
o.write(" " + ", ".join("E(%d)" % j for j in range(i, min(n, i + 10))) + ",\n")
o.write("};\n")
print("emoji: %d glyphs, %d KB" % (n, len(data) // 1024))
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])
@@ -5,7 +5,8 @@
# Czech, Slovak, Hungarian, Romanian ș/ț, Baltic, Turkish, Nordic, Icelandic,
# Maltese, Welsh ŵ/ŷ/ẁ…, German ẞ), Greek, Cyrillic (+ supplement: Ukrainian,
# Belarusian, Serbian, Macedonian, Bulgarian…), typographic punctuation,
# currency (€ ₴ ₽ …) and №, plus LVGL's LV_SYMBOL_* icons.
# currency (€ ₴ ₽ …) and №, plus LVGL's LV_SYMBOL_* icons; emoji fall
# back to colour Twemoji images (emoji.py -> ui_emoji_data.c, ui_emoji.c).
#
# Needs node (npx fetches lv_font_conv) and curl. Run from anywhere:
# examples/companion_radio/ui-lvgl/fonts/generate.sh
@@ -41,7 +42,10 @@ gen() { # size weight ranges name
--font "$CACHE/fa.woff" -r "$SYMS" \
--format lvgl --lv-include lvgl.h --lv-font-name "$4" -o "$HERE/$4.c"
}
for s in 12 14 16 20; do gen "$s" Medium "$TEXT" "ui_font_$s"; done
for s in 12 14 16 20; do
gen "$s" Medium "$TEXT" "ui_font_$s"
sed -i.bak "s/\.fallback = NULL,/.fallback = \&ui_emoji_$s,/" "$HERE/ui_font_$s.c" && rm "$HERE/ui_font_$s.c.bak"
done
# Clock: digits, colon, dash, dot, space only.
$CONV --bpp 4 --size 40 --no-compress --font "$CACHE/NotoSans-SemiBold.ttf" -r "0x20,0x2D-0x2E,0x30-0x3A" \
--format lvgl --lv-include lvgl.h --lv-font-name ui_font_40 -o "$HERE/ui_font_40.c"
@@ -54,4 +58,9 @@ $CONV --bpp 4 --size 14 --no-compress --font "$CACHE/ui-icons.ttf" -r "$ICONS" \
--format lvgl --lv-include lvgl.h --lv-font-name ui_icons_14 -o "$HERE/ui_icons_14.c"
sed -i.bak -e 's/\.line_height = [0-9]*,/.line_height = 18,/' -e 's/\.base_line = [0-9]*,/.base_line = 4,/' \
-e 's/\.fallback = NULL,/.fallback = \&ui_font_12,/' "$HERE/ui_icons_14.c" && rm "$HERE/ui_icons_14.c.bak"
# Colour emoji the text fonts fall back to (ui_emoji.c): Twemoji 15.1, CC-BY 4.0.
# Needs python3 with Pillow.
TW="$CACHE/twemoji-15.1.0"
[ -d "$TW" ] || curl -sSL https://codeload.github.com/jdecked/twemoji/tar.gz/refs/tags/v15.1.0 | tar xz -C "$CACHE" twemoji-15.1.0/assets/72x72
python3 "$HERE/emoji.py" "$TW/assets/72x72" "$HERE/ui_emoji_data.c"
echo "done:$(ls "$HERE"/ui_font_*.c | wc -l) fonts"
@@ -0,0 +1,118 @@
/* Colour emoji for the text fonts: ui_font_<size> falls back to ui_emoji_<size>
* (generate.sh sets the fallback), which draws the Twemoji images of
* ui_emoji_data.c (emoji.py). One 16 px image set; each font only centres it on
* its own line, which is why there is one lv_font_t per text size.
*
* LVGL doesn't shape text, so a sequence draws as its parts: the joiners and
* selectors between them take no space, a skin tone is dropped (the plain
* emoji stays). Flags are swapped in before drawing (ui_emoji_flags). */
#include "lvgl.h"
#include <string.h>
extern const uint16_t ui_emoji_count;
extern const uint32_t ui_emoji_cps[];
extern const lv_image_dsc_t ui_emoji_img[];
#define PX 16
#define FLAG_BASE 0xF0000 /* flag images: FLAG_BASE + letter1 * 26 + letter2 (emoji.py) */
static bool zero_width(uint32_t u) {
return u == 0x200D /* zero width joiner */
|| u == 0xFE0E || u == 0xFE0F /* text / emoji presentation */
|| u == 0x20E3 /* keycap */
|| (u >= 0x1F3FB && u <= 0x1F3FF) /* skin tones */
|| (u >= 0xE0020 && u <= 0xE007F); /* tags (subdivision flags) */
}
static int find(uint32_t u) {
if (u < ui_emoji_cps[0] || u > ui_emoji_cps[ui_emoji_count - 1]) return -1;
int lo = 0, hi = ui_emoji_count - 1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (ui_emoji_cps[mid] < u) lo = mid + 1;
else if (ui_emoji_cps[mid] > u) hi = mid - 1;
else return mid;
}
return -1;
}
/* 0..25 when p starts with a regional indicator letter (U+1F1E6 + 0..25,
* UTF-8 F0 9F 87 A6..BF), else -1. */
static int ri_letter(const char* p) {
return ((uint8_t)p[0] == 0xF0 && (uint8_t)p[1] == 0x9F && (uint8_t)p[2] == 0x87 && (uint8_t)p[3] >= 0xA6 &&
(uint8_t)p[3] <= 0xBF) ? (uint8_t)p[3] - 0xA6 : -1;
}
/* A flag is a pair of regional indicator letters. Returns s with every pair
* that has an image swapped for that image's codepoint -- a copy from lv_malloc,
* for the caller to lv_free -- or NULL when s has no flag. The copy is never
* longer: 8 bytes become 4. */
char* ui_emoji_flags(const char* s) {
if (!strstr(s, "\xF0\x9F\x87")) return NULL;
size_t n = strlen(s);
char* out = lv_malloc(n + 1);
if (!out) return NULL;
char* o = out;
while (*s) {
int a = ri_letter(s), b = a >= 0 ? ri_letter(s + 4) : -1;
if (b >= 0) {
uint32_t cp = FLAG_BASE + a * 26 + b;
if (find(cp) >= 0) {
*o++ = (char)(0xF0 | (cp >> 18));
*o++ = (char)(0x80 | ((cp >> 12) & 0x3F));
*o++ = (char)(0x80 | ((cp >> 6) & 0x3F));
*o++ = (char)(0x80 | (cp & 0x3F));
} else { /* no such flag: keep both letters, so the next pair stays paired */
memcpy(o, s, 8);
o += 8;
}
s += 8;
} else {
*o++ = *s++;
}
}
*o = 0;
return out;
}
static bool get_glyph_dsc(const lv_font_t* font, lv_font_glyph_dsc_t* g, uint32_t u, uint32_t next) {
LV_UNUSED(next);
g->is_placeholder = 0;
g->ofs_x = 0;
if (zero_width(u)) {
g->adv_w = g->box_w = g->box_h = 0;
g->ofs_y = 0;
g->format = LV_FONT_GLYPH_FORMAT_NONE;
return true;
}
int i = find(u);
if (i < 0) return false;
g->adv_w = PX + 2; /* 1 px clear each side */
g->box_w = g->box_h = PX;
g->ofs_x = 1;
g->ofs_y = (int16_t)(intptr_t)font->user_data;
g->format = LV_FONT_GLYPH_FORMAT_IMAGE;
g->gid.src = &ui_emoji_img[i];
return true;
}
static const void* get_glyph_bitmap(lv_font_glyph_dsc_t* g, lv_draw_buf_t* buf) {
LV_UNUSED(buf);
return g->gid.src;
}
/* LVGL puts a glyph's top at line top + (line_height - base_line) - box_h - ofs_y
* with the *text* font's metrics; this ofs_y centres the image on that line. */
#define EMOJI_FONT(name, lh, bl) \
const lv_font_t name = { \
.get_glyph_dsc = get_glyph_dsc, \
.get_glyph_bitmap = get_glyph_bitmap, \
.line_height = (lh), \
.base_line = (bl), \
.user_data = (void*)(intptr_t)(((lh) - (bl)) - PX - ((lh) - PX) / 2), \
};
EMOJI_FONT(ui_emoji_12, 18, 4)
EMOJI_FONT(ui_emoji_14, 20, 4)
EMOJI_FONT(ui_emoji_16, 23, 5)
EMOJI_FONT(ui_emoji_20, 28, 6)
File diff suppressed because it is too large Load Diff
@@ -10780,7 +10780,7 @@ lv_font_t ui_font_12 = {
#endif
.dsc = &font_dsc, /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */
#if LV_VERSION_CHECK(8, 2, 0) || LVGL_VERSION_MAJOR >= 9
.fallback = NULL,
.fallback = &ui_emoji_12,
#endif
.user_data = NULL,
};
@@ -12260,7 +12260,7 @@ lv_font_t ui_font_14 = {
#endif
.dsc = &font_dsc, /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */
#if LV_VERSION_CHECK(8, 2, 0) || LVGL_VERSION_MAJOR >= 9
.fallback = NULL,
.fallback = &ui_emoji_14,
#endif
.user_data = NULL,
};
@@ -14191,7 +14191,7 @@ lv_font_t ui_font_16 = {
#endif
.dsc = &font_dsc, /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */
#if LV_VERSION_CHECK(8, 2, 0) || LVGL_VERSION_MAJOR >= 9
.fallback = NULL,
.fallback = &ui_emoji_16,
#endif
.user_data = NULL,
};
@@ -18520,7 +18520,7 @@ lv_font_t ui_font_20 = {
#endif
.dsc = &font_dsc, /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */
#if LV_VERSION_CHECK(8, 2, 0) || LVGL_VERSION_MAJOR >= 9
.fallback = NULL,
.fallback = &ui_emoji_20,
#endif
.user_data = NULL,
};
+9 -2
View File
@@ -28,24 +28,31 @@
// Fonts: generated Noto Sans (European Latin, Greek, Cyrillic + LV_SYMBOL_*),
// see fonts/generate.sh. LVGL's built-in Montserrat (ASCII only) is off.
// Each text font falls back to colour emoji (fonts/ui_emoji.c, Twemoji).
#define LV_FONT_MONTSERRAT_14 0
#define LV_FONT_CUSTOM_DECLARE LV_FONT_DECLARE(ui_font_12) LV_FONT_DECLARE(ui_font_14) \
LV_FONT_DECLARE(ui_font_16) LV_FONT_DECLARE(ui_font_20) \
LV_FONT_DECLARE(ui_font_40) \
LV_FONT_DECLARE(ui_icons_14)
LV_FONT_DECLARE(ui_icons_14) \
LV_FONT_DECLARE(ui_emoji_12) LV_FONT_DECLARE(ui_emoji_14) \
LV_FONT_DECLARE(ui_emoji_16) LV_FONT_DECLARE(ui_emoji_20)
// Image glyphs (the emoji) are drawn only with this on; lv_imgfont itself is unused.
#define LV_USE_IMGFONT 1
#define LV_FONT_DEFAULT &ui_font_14
#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
// Spans: message text with @[nick] mentions picked out (UITask.cpp msgText).
#define LV_USE_SPAN 1
#define LV_USE_THEME_DEFAULT 1
#define LV_THEME_DEFAULT_DARK 1
// Not needed by the frontend (smaller build).
#define LV_USE_CHART 0
#define LV_USE_CALENDAR 0
#define LV_USE_SPAN 0
#define LV_USE_TABLE 0
#define LV_USE_SCALE 0
#define LV_USE_LOTTIE 0