mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 03:06:39 +00:00
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:
@@ -247,12 +247,18 @@ User list, second batch (2026-09-26):
|
||||
- [x] Map download popup runs off the screen; take the WiFi settings out
|
||||
of it (one place: Settings > WiFi). Compact: a progress bar, one
|
||||
line of counts, errors on one line, only the network's name.
|
||||
- [ ] Mentions: highlight @[nick] in message text (a coloured chip in
|
||||
place of the raw "@[...]" markup), our own name stronger.
|
||||
- [x] Mentions: "@[nick]" shows as "@nick" in the accent colour (a span);
|
||||
our own name underlined and its bubble outlined. Previews and the
|
||||
message popup's quote show "@nick" too.
|
||||
- [x] Splash: "MeshCore Solo" with the upstream version and ours, all in
|
||||
the MeshCore title font (the wordmark's letters plus l, v, d, digits,
|
||||
'.', '-' drawn to match; Noto for a line with anything else).
|
||||
- [ ] Emoji, ideally the full range (glyphs could live on the SD card).
|
||||
- [x] Emoji: every single-codepoint emoji plus all flags, colour Twemoji
|
||||
at 16 px baked into flash (~1.2 MB; fonts/emoji.py -> ui_emoji_data.c),
|
||||
the text fonts' fallback. Flags are swapped for one private codepoint
|
||||
before text reaches a label. Other sequences (skin tones, ZWJ) draw as
|
||||
their parts. Credit in Settings > About. Later: an emoji picker on the
|
||||
keyboard.
|
||||
- [x] Tap-to-wake toggle (Settings > Display & power > WAKE, NVS, on by
|
||||
default; off: only the top button wakes it).
|
||||
- [x] Favourites as their own screen in the menu rather than a tile: a
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user