Merge pull request #58 from torlando-tech/fix/nomadnet-unicode
Build and Deploy Firmware / build-and-deploy (push) Canceled after 0s
Test / Pyxis pytest suite (build_scripts + native) (push) Canceled after 0s
Test / microReticulum native unit tests (PlatformIO native17) (push) Canceled after 0s

fix(nomadnet): render bounded Unicode and preserve icons
This commit is contained in:
Torlando
2026-08-05 20:56:21 -04:00
committed by GitHub
11 changed files with 6560 additions and 18 deletions
+1 -1
View File
@@ -93,7 +93,7 @@
#define LV_FONT_DEFAULT &lv_font_montserrat_14
#define LV_FONT_FMT_TXT_LARGE 0
#define LV_USE_FONT_COMPRESSED 0
#define LV_USE_FONT_COMPRESSED 1 /* Required by generated NomadNet font bitmaps */
#define LV_USE_FONT_SUBPX 0
/*====================
+46
View File
@@ -0,0 +1,46 @@
DejaVu Fonts License
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.
Bitstream Vera is a trademark of Bitstream, Inc.
DejaVu changes are in public domain.
Permission is hereby granted, free of charge, to any person obtaining a copy
of the fonts accompanying this license ("Fonts") and associated
documentation files (the "Font Software"), to reproduce and distribute the
Font Software, including without limitation the rights to use, copy, merge,
publish, distribute, and/or sell copies of the Font Software, and to permit
persons to whom the Font Software is furnished to do so, subject to the
following conditions:
The above copyright and trademark notices and this permission notice shall
be included in all copies of one or more of the Font Software typefaces.
The Font Software may be modified, altered, or added to, and in particular
the designs of glyphs or characters in the Fonts may be modified and
additional glyphs or characters may be added to the Fonts, only if the fonts
are renamed to names not containing either the words "Bitstream" or the word
"Vera".
This License becomes null and void to the extent applicable to Fonts or Font
Software that has been modified and is distributed under the "Bitstream
Vera" names.
The Font Software may be sold as part of a larger software package but no
copy of one or more of the Font Software typefaces may be sold by itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, CONSEQUENTIAL DAMAGES, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR
INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT
SOFTWARE.
Except as contained in this notice, the names of Gnome, the Gnome
Foundation, and Bitstream Inc., shall not be used in advertising or
otherwise to promote the sale, use or other dealings in this Font Software
without prior written authorization from the Gnome Foundation or Bitstream
Inc., respectively. For further information, contact: fonts at gnome dot
org.
@@ -0,0 +1,32 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
// Exact codepoint contract shared by the generated LVGL font adapters and the
// host-testable UTF-8 display sanitizer. Keeping this separate prevents LVGL's
// inclusive range-length check from treating the first codepoint after a
// contiguous cmap as a valid (and potentially out-of-bounds) glyph.
static inline bool nomadnet_font_has_codepoint(uint32_t codepoint) {
if (codepoint >= 0x20 && codepoint <= 0x7e) return true;
if (codepoint >= 0x00a0 && codepoint <= 0x017f) return true;
if (codepoint >= 0x2190 && codepoint <= 0x2199) return true;
switch (codepoint) {
case 0x2007: case 0x2008: case 0x2009: case 0x200a: case 0x200b:
case 0x2010: case 0x2012: case 0x2013: case 0x2014: case 0x2015:
case 0x2018: case 0x2019: case 0x201a: case 0x201c: case 0x201d:
case 0x201e: case 0x2020: case 0x2021: case 0x2022: case 0x2026:
case 0x2030: case 0x2032: case 0x2033: case 0x2039: case 0x203a:
case 0x2044: case 0x2052:
case 0x20a1: case 0x20a3: case 0x20a4: case 0x20a6: case 0x20a7:
case 0x20a9: case 0x20ab: case 0x20ac: case 0x20ad: case 0x20ae:
case 0x20b1: case 0x20b2: case 0x20b4: case 0x20b5: case 0x20b8:
case 0x20b9: case 0x20ba: case 0x20bc: case 0x20bd:
case 0x2500: case 0x2550: case 0x2551: case 0x2554: case 0x2557:
case 0x255a: case 0x255d: case 0x2588: case 0x2594: case 0x25a0:
return true;
default:
return false;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+77
View File
@@ -0,0 +1,77 @@
#include "NomadNetGlyphs.h"
#include "../Fonts/NomadNetFontCoverage.h"
#include <cstddef>
#include <cstdint>
namespace UI::LXMF::NomadNet {
namespace {
bool decode_utf8(const std::string& value, std::size_t offset,
uint32_t& codepoint, std::size_t& length) {
const auto lead = static_cast<uint8_t>(value[offset]);
if (lead <= 0x7f) {
codepoint = lead;
length = 1;
return true;
}
std::size_t continuation = 0;
if (lead >= 0xc2 && lead <= 0xdf) {
codepoint = lead & 0x1f;
continuation = 1;
} else if (lead >= 0xe0 && lead <= 0xef) {
codepoint = lead & 0x0f;
continuation = 2;
} else if (lead >= 0xf0 && lead <= 0xf4) {
codepoint = lead & 0x07;
continuation = 3;
} else {
length = 1;
return false;
}
length = continuation + 1;
if (offset + length > value.size()) {
length = 1;
return false;
}
for (std::size_t i = 1; i <= continuation; ++i) {
const auto byte = static_cast<uint8_t>(value[offset + i]);
if ((byte & 0xc0) != 0x80) {
length = 1;
return false;
}
codepoint = (codepoint << 6) | (byte & 0x3f);
}
if ((continuation == 1 && codepoint < 0x80) ||
(continuation == 2 && codepoint < 0x800) ||
(continuation == 3 && codepoint < 0x10000) ||
(codepoint >= 0xd800 && codepoint <= 0xdfff) ||
codepoint > 0x10ffff) {
length = 1;
return false;
}
return true;
}
} // namespace
std::string display_text(const std::string& utf8) {
std::string output;
output.reserve(utf8.size());
for (std::size_t offset = 0; offset < utf8.size();) {
uint32_t codepoint = 0;
std::size_t length = 1;
if (!decode_utf8(utf8, offset, codepoint, length) ||
!nomadnet_font_has_codepoint(codepoint)) {
output.push_back('?');
} else {
output.append(utf8, offset, length);
}
offset += length;
}
return output;
}
} // namespace UI::LXMF::NomadNet
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <string>
#ifdef ARDUINO
#include <lvgl.h>
LV_FONT_DECLARE(nomadnet_font_12)
LV_FONT_DECLARE(nomadnet_font_16)
#endif
namespace UI::LXMF::NomadNet {
// The bounded browser fonts contain printable ASCII, all glyphs in
// U+00A0-U+017F, and selected available glyphs from U+2000-U+206F,
// U+20A0-U+20CF, U+2190-U+21FF, and ten exact box/block glyphs observed on
// live NomadNet pages. Other valid Unicode is replaced at the display boundary
// so LVGL never draws its missing-glyph rectangle.
std::string display_text(const std::string& utf8);
} // namespace UI::LXMF::NomadNet
+29 -17
View File
@@ -2,6 +2,7 @@
#ifdef ARDUINO
#include "Theme.h"
#include "NomadNetDisplay.h"
#include "NomadNetGlyphs.h"
#include "../LVGL/LVGLInit.h"
#include "../TextAreaHelper.h"
#include <algorithm>
@@ -33,13 +34,13 @@ NomadNetScreen::NomadNetScreen() {
lv_obj_set_style_bg_color(_address_row,Theme::surface(),0);lv_obj_set_style_border_width(_address_row,0,0);lv_obj_set_style_pad_all(_address_row,3,0);
_address=lv_textarea_create(_address_row);lv_obj_set_size(_address,270,30);lv_obj_align(_address,LV_ALIGN_LEFT_MID,0,0);
lv_textarea_set_one_line(_address,true);lv_textarea_set_max_length(_address,511);lv_textarea_set_placeholder_text(_address,"destination:/page/path");
lv_obj_set_style_text_font(_address,&lv_font_montserrat_12,0);lv_obj_set_style_bg_color(_address,Theme::surfaceInput(),0);TextAreaHelper::enable_paste(_address);
lv_obj_set_style_text_font(_address,&nomadnet_font_12,0);lv_obj_set_style_bg_color(_address,Theme::surfaceInput(),0);TextAreaHelper::enable_paste(_address);
_go_button=lv_btn_create(_address_row);lv_obj_set_size(_go_button,42,30);lv_obj_align(_go_button,LV_ALIGN_RIGHT_MID,0,0);
lv_obj_set_style_bg_color(_go_button,Theme::primary(),0);lv_obj_set_style_radius(_go_button,8,0);
lv_obj_t* gl=lv_label_create(_go_button);lv_label_set_text(gl,"Go");lv_obj_center(gl);
_address_summary=lv_label_create(_address_row);lv_obj_set_size(_address_summary,266,24);lv_obj_align(_address_summary,LV_ALIGN_LEFT_MID,4,0);
lv_label_set_long_mode(_address_summary,LV_LABEL_LONG_DOT);lv_obj_set_style_text_font(_address_summary,&lv_font_montserrat_12,0);
lv_label_set_long_mode(_address_summary,LV_LABEL_LONG_DOT);lv_obj_set_style_text_font(_address_summary,&nomadnet_font_12,0);
lv_obj_set_style_text_color(_address_summary,Theme::textSecondary(),0);
_edit_button=lv_btn_create(_address_row);lv_obj_set_size(_edit_button,36,26);lv_obj_align(_edit_button,LV_ALIGN_RIGHT_MID,0,0);
lv_obj_set_style_bg_color(_edit_button,Theme::surfaceContainer(),0);lv_obj_set_style_bg_color(_edit_button,Theme::primaryPressed(),LV_STATE_FOCUSED);
@@ -65,7 +66,7 @@ NomadNetScreen::NomadNetScreen() {
NomadNetScreen::~NomadNetScreen(){if(_screen)lv_obj_del(_screen);}
void NomadNetScreen::set_address(const std::string& value){
lv_textarea_set_text(_address,value.c_str());
const auto summary=NomadNet::compact_address(value);
const auto summary=NomadNet::display_text(NomadNet::compact_address(value));
lv_label_set_text(_address_summary,summary.c_str());
}
std::string NomadNetScreen::address()const{return lv_textarea_get_text(_address);}
@@ -117,15 +118,24 @@ void NomadNetScreen::render_directory(View view){
lv_obj_clear_flag(_directory,LV_OBJ_FLAG_HIDDEN);
for(auto* object:{_address_row,_status,_content,_reload_button,_save_button})lv_obj_add_flag(object,LV_OBJ_FLAG_HIDDEN);
auto add_row=[&](const std::string& title,const std::string& detail,std::size_t code){
auto add_row=[&](const std::string& title,const std::string& detail,std::size_t code,const char* symbol=nullptr){
lv_obj_t* button=lv_btn_create(_directory);lv_obj_set_size(button,306,35);lv_obj_set_flex_grow(button,0);
lv_obj_set_style_bg_color(button,Theme::surfaceContainer(),0);lv_obj_set_style_bg_color(button,Theme::primaryPressed(),LV_STATE_FOCUSED);
lv_obj_set_style_border_width(button,0,0);lv_obj_set_style_radius(button,8,0);lv_obj_set_style_pad_all(button,4,0);
lv_obj_t* primary=lv_label_create(button);lv_label_set_text(primary,title.c_str());lv_label_set_long_mode(primary,LV_LABEL_LONG_DOT);
lv_obj_set_width(primary,286);lv_obj_set_style_text_font(primary,&lv_font_montserrat_12,0);lv_obj_align(primary,LV_ALIGN_TOP_LEFT,2,0);
lv_coord_t title_x=2;
lv_coord_t title_width=286;
if(symbol){
lv_obj_t* icon=lv_label_create(button);lv_label_set_text(icon,symbol);
lv_obj_set_style_text_font(icon,&lv_font_montserrat_12,0);lv_obj_align(icon,LV_ALIGN_TOP_LEFT,2,0);
title_x=20;title_width=268;
}
const auto rendered_title=NomadNet::display_text(title);
lv_obj_t* primary=lv_label_create(button);lv_label_set_text(primary,rendered_title.c_str());lv_label_set_long_mode(primary,LV_LABEL_LONG_DOT);
lv_obj_set_width(primary,title_width);lv_obj_set_style_text_font(primary,&nomadnet_font_12,0);lv_obj_align(primary,LV_ALIGN_TOP_LEFT,title_x,0);
if(!detail.empty()){
lv_obj_t* secondary=lv_label_create(button);lv_label_set_text(secondary,detail.c_str());lv_label_set_long_mode(secondary,LV_LABEL_LONG_DOT);
lv_obj_set_width(secondary,286);lv_obj_set_style_text_font(secondary,&lv_font_montserrat_12,0);lv_obj_set_style_text_color(secondary,Theme::textTertiary(),0);
const auto rendered_detail=NomadNet::display_text(detail);
lv_obj_t* secondary=lv_label_create(button);lv_label_set_text(secondary,rendered_detail.c_str());lv_label_set_long_mode(secondary,LV_LABEL_LONG_DOT);
lv_obj_set_width(secondary,286);lv_obj_set_style_text_font(secondary,&nomadnet_font_12,0);lv_obj_set_style_text_color(secondary,Theme::textTertiary(),0);
lv_obj_align(secondary,LV_ALIGN_BOTTOM_LEFT,2,0);
}
lv_obj_set_user_data(button,reinterpret_cast<void*>(code));lv_obj_add_event_cb(button,clicked,LV_EVENT_CLICKED,this);
@@ -133,11 +143,11 @@ void NomadNetScreen::render_directory(View view){
};
if(view==View::START){
add_row(LV_SYMBOL_DIRECTORY " Heard Nodes","Recently announced NomadNet nodes",1001);
add_row(LV_SYMBOL_DIRECTORY " Saved Nodes","Bookmarked destinations",1002);
add_row(LV_SYMBOL_SAVE " Saved Pages","Bookmarked destination and path",1003);
add_row(LV_SYMBOL_LIST " Recent Pages","Bounded browsing history",1004);
add_row(LV_SYMBOL_EDIT " Enter Address","Advanced destination/path entry",1005);
add_row("Heard Nodes","Recently announced NomadNet nodes",1001,LV_SYMBOL_DIRECTORY);
add_row("Saved Nodes","Bookmarked destinations",1002,LV_SYMBOL_DIRECTORY);
add_row("Saved Pages","Bookmarked destination and path",1003,LV_SYMBOL_SAVE);
add_row("Recent Pages","Bounded browsing history",1004,LV_SYMBOL_LIST);
add_row("Enter Address","Advanced destination/path entry",1005,LV_SYMBOL_EDIT);
}else if(view==View::HEARD||view==View::SAVED_NODES){
for(const auto& node:_library.nodes()){
if(view==View::SAVED_NODES&&!node.saved)continue;
@@ -159,7 +169,7 @@ void NomadNetScreen::render_directory(View view){
if(_directory_focusables.empty()){
lv_obj_t* empty=lv_label_create(_directory);
lv_label_set_text(empty,view==View::HEARD?"No NomadNet nodes heard yet":view==View::SAVED_NODES?"No saved nodes":view==View::SAVED_PAGES?"No saved pages":"No recent pages");
lv_obj_set_style_text_color(empty,Theme::textTertiary(),0);lv_obj_set_style_text_font(empty,&lv_font_montserrat_12,0);
lv_obj_set_style_text_color(empty,Theme::textTertiary(),0);lv_obj_set_style_text_font(empty,&nomadnet_font_12,0);
}
rebuild_focus();
}
@@ -265,7 +275,8 @@ void NomadNetScreen::set_page(const NomadNet::Document& document) {
for (const auto& run : block.runs) {
if (spans >= MAX_UI_SPANS) { render_truncated = true; break; }
lv_span_t* span = lv_spangroup_new_span(text);
lv_span_set_text(span, run.text.c_str());
const auto rendered_text = NomadNet::display_text(run.text);
lv_span_set_text(span, rendered_text.c_str());
const bool is_link = run.link_index >= 0 &&
static_cast<std::size_t>(run.link_index) < document.links.size();
lv_style_set_text_color(&span->style, is_link ? Theme::primaryLight() :
@@ -273,7 +284,7 @@ void NomadNetScreen::set_page(const NomadNet::Document& document) {
document.has_foreground ? lv_color_hex(document.foreground) : Theme::textPrimary());
lv_style_set_text_font(&span->style,
(run.bold || block.type == NomadNet::BlockType::HEADING)
? &lv_font_montserrat_16 : &lv_font_montserrat_12);
? &nomadnet_font_16 : &nomadnet_font_12);
lv_style_set_text_line_space(&span->style, 3);
if (run.underline || is_link)
lv_style_set_text_decor(&span->style, LV_TEXT_DECOR_UNDERLINE);
@@ -292,8 +303,9 @@ void NomadNetScreen::set_page(const NomadNet::Document& document) {
lv_obj_set_size(button, 304, 28);
lv_obj_set_style_pad_all(button, 4, 0);
lv_obj_t* label = lv_label_create(button);
const std::string caption = "Open: " + run.text;
const std::string caption = "Open: " + NomadNet::display_text(run.text);
lv_label_set_text(label, caption.c_str());
lv_obj_set_style_text_font(label, &nomadnet_font_12, 0);
lv_label_set_long_mode(label, LV_LABEL_LONG_DOT);
lv_obj_set_width(label, 294);
lv_obj_center(label);
+1
View File
@@ -19,6 +19,7 @@
"+<Hardware/TDeck/*.cpp>",
"+<UI/*.cpp>",
"+<UI/LVGL/*.cpp>",
"+<UI/Fonts/*.c>",
"+<UI/LXMF/*.cpp>"
],
"includeDir": "."
@@ -10,6 +10,7 @@
#include "NomadNetDocument.h"
#include "NomadNetDisplay.h"
#include "NomadNetHistory.h"
#include "NomadNetGlyphs.h"
#include "NomadNetLibrary.h"
#include "NomadNetActionMailbox.h"
#include "NomadNetMailbox.h"
@@ -24,6 +25,7 @@ using UI::LXMF::NomadNet::BlockType;
using UI::LXMF::NomadNet::DocumentParser;
using UI::LXMF::NomadNet::compact_address;
using UI::LXMF::NomadNet::PageHistory;
using UI::LXMF::NomadNet::display_text;
using UI::LXMF::NomadNet::Library;
using UI::LXMF::NomadNet::ActionMailbox;
using UI::LXMF::NomadNet::UserAction;
@@ -170,6 +172,17 @@ int main(int argc, char** argv) {
auto utf8_doc = parser.parse(invalid_utf8);
check("invalid UTF-8 is rejected before rendering", utf8_doc.malformed && utf8_doc.blocks.empty());
check("common NomadNet Unicode punctuation remains intact",
display_text(u8"release · stable — open → details • done") ==
u8"release · stable — open → details • done");
check("Latin accents remain intact", display_text(u8"café Ångström") == u8"café Ångström");
check("live NomadNet box and block drawing glyphs remain intact",
display_text(u8"─═║╔╗╚╝█▔■") == u8"─═║╔╗╚╝█▔■");
check("glyphs outside the bounded browser font degrade without rectangles",
display_text(u8"status 😀 ok") == "status ? ok");
check("contiguous cmap boundary codepoints degrade safely",
display_text(std::string("\x7f") + u8"ƀ↚") == "???");
std::string huge(DocumentParser::MAX_DOCUMENT_BYTES, 'x');
huge += "\n#!c=99\n";
auto huge_doc = parser.parse(huge);
@@ -1,6 +1,7 @@
import shutil
import subprocess
import hashlib
import re
from pathlib import Path
import pytest
@@ -36,6 +37,7 @@ def test_app_launcher_nomadnet_native(tmp_path):
_cxx(), "-std=c++17", "-Wall", "-Wextra", "-Werror",
f"-I{INCLUDE}", str(SOURCE),
str(INCLUDE / "NomadNetDocument.cpp"),
str(INCLUDE / "NomadNetGlyphs.cpp"),
str(INCLUDE / "NomadNetLibrary.cpp"),
str(INCLUDE / "NomadNetUrl.cpp"),
"-o", str(binary),
@@ -49,11 +51,13 @@ def test_app_launcher_nomadnet_native(tmp_path):
def test_ui_wiring_contract():
library_json = (ROOT / "lib" / "tdeck_ui" / "library.json").read_text()
manager_h = (INCLUDE / "UIManager.h").read_text()
manager_cpp = (INCLUDE / "UIManager.cpp").read_text()
launcher_cpp = (INCLUDE / "HomeScreen.cpp").read_text()
network_cpp = (INCLUDE / "NetworkScreen.cpp").read_text()
browser_cpp = (INCLUDE / "NomadNetScreen.cpp").read_text()
glyphs_h = (INCLUDE / "NomadNetGlyphs.h").read_text()
for tile in ("Messages", "NomadNet", "Network", "Settings"):
assert tile in launcher_cpp
@@ -92,6 +96,13 @@ def test_ui_wiring_contract():
assert "lv_group_focus_obj(_focusables.front())" not in browser_cpp
assert "LV_SYMBOL_HOME" in browser_cpp
assert "lv_font_" in browser_cpp
assert "nomadnet_font_12" in browser_cpp
assert "nomadnet_font_16" in browser_cpp
assert "display_text" in browser_cpp
assert "U+00A0-U+017F" in glyphs_h
assert "U+2000-U+206F" in glyphs_h
assert "U+2190-U+21FF" in glyphs_h
assert "+<UI/Fonts/*.c>" in library_json
for detail in ("Inbox & calls", "Browse Micron", "Links & radio", "Device options"):
assert detail in launcher_cpp
for detail in ("Interfaces & storage", "Signal history", "Delivery relays"):
@@ -106,6 +117,52 @@ def test_ui_wiring_contract():
assert "set_save_callback" in manager_cpp
def test_bounded_nomadnet_fonts_match_display_allowlist():
expected = set(range(0x20, 0x7F)) | set(range(0xA0, 0x180))
expected |= set(range(0x2190, 0x219A))
expected |= {
0x2007, 0x2008, 0x2009, 0x200A, 0x200B,
0x2010, 0x2012, 0x2013, 0x2014, 0x2015,
0x2018, 0x2019, 0x201A, 0x201C, 0x201D, 0x201E,
0x2020, 0x2021, 0x2022, 0x2026, 0x2030, 0x2032,
0x2033, 0x2039, 0x203A, 0x2044, 0x2052,
0x20A1, 0x20A3, 0x20A4, 0x20A6, 0x20A7, 0x20A9,
0x20AB, 0x20AC, 0x20AD, 0x20AE, 0x20B1, 0x20B2,
0x20B4, 0x20B5, 0x20B8, 0x20B9, 0x20BA, 0x20BC, 0x20BD,
0x2500, 0x2550, 0x2551, 0x2554, 0x2557,
0x255A, 0x255D, 0x2588, 0x2594, 0x25A0,
}
for size in (12, 16):
source = (ROOT / "lib" / "tdeck_ui" / "UI" / "Fonts" /
f"nomadnet_font_{size}.c").read_text()
encoded = {int(value, 16) for value in re.findall(r"/\* U\+([0-9A-F]+)", source)}
assert encoded == expected
assert f".get_glyph_dsc = nomadnet_font_{size}_get_glyph_dsc" in source
assert f".get_glyph_bitmap = nomadnet_font_{size}_get_glyph_bitmap" in source
assert "nomadnet_font_has_codepoint(letter)" in source
def test_generated_font_compression_is_enabled_in_lvgl():
config = (ROOT / "lib" / "lv_conf.h").read_text()
for size in (12, 16):
source = (ROOT / "lib" / "tdeck_ui" / "UI" / "Fonts" /
f"nomadnet_font_{size}.c").read_text()
assert ".bitmap_format = 1" in source
assert "#if !LV_USE_FONT_COMPRESSED" in source
assert "#error \"NomadNet fonts require LV_USE_FONT_COMPRESSED=1\"" in source
assert "#define LV_USE_FONT_COMPRESSED 1" in config
def test_directory_icons_use_lvgl_symbol_font_separately_from_remote_text():
screen = (INCLUDE / "NomadNetScreen.cpp").read_text()
assert "add_row(LV_SYMBOL" not in screen
assert "const char* symbol=nullptr" in screen
assert "lv_label_set_text(icon,symbol)" in screen
assert "lv_obj_set_style_text_font(icon,&lv_font_montserrat_12,0)" in screen
for symbol in ("LV_SYMBOL_DIRECTORY", "LV_SYMBOL_SAVE", "LV_SYMBOL_LIST", "LV_SYMBOL_EDIT"):
assert f",{symbol});" in screen
def test_nomadnet_latency_and_path_lifecycle_contracts():
manager_cpp = (INCLUDE / "UIManager.cpp").read_text()
main_cpp = (ROOT / "src" / "main.cpp").read_text()