diff --git a/lib/EpdFont/SdCardFontManager.cpp b/lib/EpdFont/SdCardFontManager.cpp index a6032336..804df07d 100644 --- a/lib/EpdFont/SdCardFontManager.cpp +++ b/lib/EpdFont/SdCardFontManager.cpp @@ -28,25 +28,21 @@ int SdCardFontManager::computeFontId(uint32_t contentHash, const char* familyNam return id != 0 ? id : 1; // 0 is reserved as "not found" sentinel } -bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum) { +bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t targetPtSize) { // Unload any previously loaded family first if (!loadedFamilyName_.empty()) { unloadAll(renderer); } - // Select by ordinal position: sort available sizes, then map the font size - // enum (SMALL=0 .. EXTRA_LARGE=3) to the corresponding slot. When the - // family has fewer sizes than 4, clamp to the last available size. - auto sizes = family.availableSizes(); - if (sizes.empty()) { + // Pick the single file whose size is closest to targetPtSize. Loading + // only one size bounds resident memory (intervals + kern/ligature tables + // per style) to one file's worth, vs. N_sizes × per-file overhead. + const SdCardFontFileInfo* selected = family.pickClosestSize(targetPtSize); + if (!selected) { LOG_ERR("SDMGR", "Family %s has no files to load", family.name.c_str()); return false; } - uint8_t idx = fontSizeEnum; - if (idx >= sizes.size()) idx = sizes.size() - 1; - const SdCardFontFileInfo* selected = family.findFile(sizes[idx]); - auto* font = new (std::nothrow) SdCardFont(); if (!font) { LOG_ERR("SDMGR", "Failed to allocate SdCardFont for %s", selected->path.c_str()); @@ -70,8 +66,8 @@ bool SdCardFontManager::loadFamily(const SdCardFontFamilyInfo& family, GfxRender renderer.registerSdCardFont(fontId, font); loaded_.push_back({font, fontId, selected->pointSize}); - LOG_DBG("SDMGR", "Loaded %s size=%u id=%d styles=%u (sizeEnum=%u)", selected->path.c_str(), selected->pointSize, - fontId, font->styleCount(), fontSizeEnum); + LOG_DBG("SDMGR", "Loaded %s size=%u id=%d styles=%u (target=%u)", selected->path.c_str(), selected->pointSize, fontId, + font->styleCount(), targetPtSize); EpdFontFamily fontFamily(font->getEpdFont(0), font->getEpdFont(1), font->getEpdFont(2), font->getEpdFont(3)); renderer.insertFont(fontId, fontFamily); diff --git a/lib/EpdFont/SdCardFontManager.h b/lib/EpdFont/SdCardFontManager.h index aec07472..def66e8d 100644 --- a/lib/EpdFont/SdCardFontManager.h +++ b/lib/EpdFont/SdCardFontManager.h @@ -15,12 +15,16 @@ class SdCardFontManager { SdCardFontManager(const SdCardFontManager&) = delete; SdCardFontManager& operator=(const SdCardFontManager&) = delete; - // Load the font file matching fontSizeEnum (SMALL=0 .. EXTRA_LARGE=3) by - // ordinal position in the family's sorted size list. Only one .cpfont file - // is loaded; other sizes remain on disk. This keeps resident interval + - // kern/ligature tables to one size's worth of memory. + // Load the single size whose pointSize is closest to targetPtSize. Only one + // .cpfont file is loaded; other sizes remain on disk. This keeps resident + // interval + kern/ligature tables to one size's worth of memory. + // + // Closest-pt selection is robust against families that don't ship the + // canonical {12,14,16,18}: a family with only [10,14,18] still resolves + // any reasonable target, where ordinal slot-mapping by SMALL..EXTRA_LARGE + // would mis-select. // Returns true on success. - bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t fontSizeEnum); + bool loadFamily(const SdCardFontFamilyInfo& family, GfxRenderer& renderer, uint8_t targetPtSize); // Unload everything, unregister from renderer. void unloadAll(GfxRenderer& renderer); diff --git a/lib/EpdFont/SdCardFontRegistry.cpp b/lib/EpdFont/SdCardFontRegistry.cpp index 2e0f145c..af1009b0 100644 --- a/lib/EpdFont/SdCardFontRegistry.cpp +++ b/lib/EpdFont/SdCardFontRegistry.cpp @@ -4,6 +4,8 @@ #include #include +#include +#include #include // --- SdCardFontFamilyInfo helpers --- @@ -38,6 +40,21 @@ std::vector SdCardFontFamilyInfo::availableSizes() const { return sizes; } +const SdCardFontFileInfo* SdCardFontFamilyInfo::pickClosestSize(uint8_t targetPtSize) const { + const SdCardFontFileInfo* selected = nullptr; + int bestDiff = INT_MAX; + for (const auto& f : files) { + int diff = std::abs(static_cast(f.pointSize) - static_cast(targetPtSize)); + // Strict < ensures the first scan wins on ties; then tie-break by smaller + // pointSize to make the choice independent of filesystem enumeration order. + if (diff < bestDiff || (diff == bestDiff && selected && f.pointSize < selected->pointSize)) { + bestDiff = diff; + selected = &f; + } + } + return selected; +} + // --- SdCardFontRegistry --- bool SdCardFontRegistry::parseFilename(const char* filename, uint8_t& size, uint8_t& style) { diff --git a/lib/EpdFont/SdCardFontRegistry.h b/lib/EpdFont/SdCardFontRegistry.h index f96035ed..aae932d9 100644 --- a/lib/EpdFont/SdCardFontRegistry.h +++ b/lib/EpdFont/SdCardFontRegistry.h @@ -20,6 +20,15 @@ struct SdCardFontFamilyInfo { const SdCardFontFileInfo* findFile(uint8_t size, uint8_t style = 0) const; bool hasSize(uint8_t size) const; std::vector availableSizes() const; + + // Pick the file whose pointSize is closest to targetPtSize. On ties (equal + // distance) prefers the smaller pointSize so behaviour is deterministic + // across SD card layouts. Returns nullptr when files is empty. + // + // Robust against families that don't ship the canonical {12,14,16,18} set: + // a family with only [10,14,18] resolves target 12 → 10 (or 14 on tie), + // target 16 → 14 or 18, etc., instead of mis-indexing by ordinal slot. + const SdCardFontFileInfo* pickClosestSize(uint8_t targetPtSize) const; }; class SdCardFontRegistry { diff --git a/lib/EpdFont/scripts/build-sd-fonts.py b/lib/EpdFont/scripts/build-sd-fonts.py index 50bc67a8..9f29c00d 100755 --- a/lib/EpdFont/scripts/build-sd-fonts.py +++ b/lib/EpdFont/scripts/build-sd-fonts.py @@ -85,15 +85,29 @@ def extract_static_instance(source_path: Path, axes: dict, family_name: str, sty tmp_fd, tmp_name = tempfile.mkstemp(suffix=".ttf", dir=cached.parent) os.close(tmp_fd) tmp_path = Path(tmp_name) - font = TTFont(str(source_path)) + # Keep separate handles for the source variable font and the static + # instance: instantiateVariableFont with default inplace=False returns a + # *new* TTFont, so rebinding `font` would otherwise strand the source's + # file handle open until GC runs. + # + # updateFontNames=True — rewrite the name table so the saved font + # reports its weight/style accurately rather + # than retaining the variable-font names. + # optimize=False — skip the gvar interpolation optimisation; + # fully pinning every axis drops gvar anyway, + # so the work would be wasted. + source_font = TTFont(str(source_path)) try: - instantiateVariableFont(font, axes) - font.save(str(tmp_path)) + font = instantiateVariableFont(source_font, axes, updateFontNames=True, optimize=False) + try: + font.save(str(tmp_path)) + finally: + font.close() except Exception: tmp_path.unlink(missing_ok=True) raise finally: - font.close() + source_font.close() tmp_path.replace(cached) return cached diff --git a/lib/EpdFont/scripts/fontconvert.py b/lib/EpdFont/scripts/fontconvert.py index 2b904eca..8e7b17a9 100755 --- a/lib/EpdFont/scripts/fontconvert.py +++ b/lib/EpdFont/scripts/fontconvert.py @@ -8,6 +8,12 @@ import argparse from collections import namedtuple from fontTools.ttLib import TTFont +# Force UTF-8 stdout so that `python fontconvert.py … > foo.h` on Windows +# (default cp1252) doesn't emit UTF-16 LE / replacement chars in the generated +# header. Wrapped in a hasattr guard so it's a no-op on older Pythons. +if hasattr(sys.stdout, 'reconfigure'): + sys.stdout.reconfigure(encoding='utf-8') + # Originally from https://github.com/vroland/epdiy parser = argparse.ArgumentParser(description="Generate a header file from a font to be used with epdiy.") @@ -792,6 +798,15 @@ if compress: # are grouped together for efficient LRU caching on the embedded target. # Since glyphs are in codepoint order, glyphs in the same Unicode block # are contiguous in the array and form natural groups. + # + # On top of script boundaries, a hard size cap (GROUP_MAX_UNCOMPRESSED_BYTES) + # is applied: if adding the next glyph would push the uncompressed group + # size over the cap, the group is closed and a new one started with the + # same script ID. This bounds the embedded decompressor's transient + # malloc regardless of font density (CJK, Vietnamese, user-supplied + # fonts with large Unicode blocks). Without it, a single dense script + # group can balloon past what fits in a transient page-decompress + # allocation on the device. SCRIPT_GROUP_RANGES = [ (0x0000, 0x007F), # ASCII (0x0080, 0x00FF), # Latin-1 Supplement @@ -809,6 +824,11 @@ if compress: (0xFFFD, 0xFFFD), # Replacement Character ] + # 64 KB cap: large enough to hold any single built-in script group with + # headroom, small enough to be a comfortable transient malloc on the + # ESP32-C3. + GROUP_MAX_UNCOMPRESSED_BYTES = 65536 + def get_script_group(code_point): for i, (start, end) in enumerate(SCRIPT_GROUP_RANGES): if start <= code_point <= end: @@ -819,17 +839,34 @@ if compress: current_group_id = None group_start = 0 group_count = 0 + group_uncompressed = 0 - for i, (props, packed) in enumerate(all_glyphs): + for i, (props, _) in enumerate(all_glyphs): sg = get_script_group(props.code_point) - if sg != current_group_id: + # Use the byte-aligned size (4-pixel-aligned row stride) rather than + # the packed length, since the decompressor consumes byte-aligned + # buffers. Empty glyphs contribute zero. + glyph_aligned_size = (((props.width + 3) // 4) * props.height + if props.width > 0 and props.height > 0 else 0) + if glyph_aligned_size > GROUP_MAX_UNCOMPRESSED_BYTES: + raise ValueError( + f"Glyph {i} (code point U+{props.code_point:04X}) byte-aligned size " + f"{glyph_aligned_size} exceeds GROUP_MAX_UNCOMPRESSED_BYTES=" + f"{GROUP_MAX_UNCOMPRESSED_BYTES}. Consider: (1) increasing GROUP_MAX_UNCOMPRESSED_BYTES, " + f"(2) reducing font size, or (3) excluding this codepoint." + ) + size_overflow = group_uncompressed + glyph_aligned_size > GROUP_MAX_UNCOMPRESSED_BYTES + + if sg != current_group_id or size_overflow: if group_count > 0: groups.append((group_start, group_count)) current_group_id = sg group_start = i group_count = 1 + group_uncompressed = glyph_aligned_size else: group_count += 1 + group_uncompressed += glyph_aligned_size if group_count > 0: groups.append((group_start, group_count)) diff --git a/lib/EpdFont/scripts/fontconvert_sdcard.py b/lib/EpdFont/scripts/fontconvert_sdcard.py index 888f9532..baf3875f 100755 --- a/lib/EpdFont/scripts/fontconvert_sdcard.py +++ b/lib/EpdFont/scripts/fontconvert_sdcard.py @@ -26,6 +26,7 @@ import freetype import struct import sys import os +import re import math import argparse from collections import namedtuple @@ -75,17 +76,39 @@ INTERVAL_PRESETS = { (0xFB00, 0xFB06)], } +# Regex for parsing unnamed hex range intervals: (0xSTART-0xEND) +_HEX_RANGE_PATTERN = re.compile(r'^\(0x([0-9a-fA-F]+)-0x([0-9a-fA-F]+)\)$') + +def parse_hex_range(s: str) -> tuple[int, int] | None: + match = _HEX_RANGE_PATTERN.fullmatch(s) + if not match: + return None + + start_hex, end_hex = match.groups() + start, end = int(start_hex, 16), int(end_hex, 16) + + # Validating Unicode range bounds. + if start > end or end > 0x10FFFF: + return None + return start, end + def resolve_intervals(preset_str): """Resolve comma-separated preset names into a merged, sorted, deduplicated interval list.""" all_intervals = [] for name in preset_str.split(","): name = name.strip().lower() - if name not in INTERVAL_PRESETS: + unnamed_interval = parse_hex_range(name) + if name not in INTERVAL_PRESETS and unnamed_interval is None: print(f"Error: unknown interval preset '{name}'", file=sys.stderr) print(f"Available presets: {', '.join(sorted(INTERVAL_PRESETS.keys()))}", file=sys.stderr) + print("You can also specify unnamed hex ranges like (0x2100-0x214F)", file=sys.stderr) sys.exit(1) - all_intervals.extend(INTERVAL_PRESETS[name]) + + if unnamed_interval is not None: + all_intervals.append(unnamed_interval) + else: + all_intervals.extend(INTERVAL_PRESETS[name]) # Always add replacement character all_intervals.append((0xFFFD, 0xFFFD)) @@ -266,11 +289,29 @@ def extract_kerning_fonttools(font_path, codepoints, ppem): lookup = gpos.LookupList.Lookup[li] for st in lookup.SubTable: actual = st - # Unwrap Extension (lookup type 9) wrappers + # Unwrap Extension (lookup type 9) wrappers. After unwrapping, + # `lookup.LookupType` is still 9, so we must look at the + # *effective* type carried on the extension subtable to know + # whether `actual` is a PairPos table. if lookup.LookupType == 9 and hasattr(st, 'ExtSubTable'): actual = st.ExtSubTable + effective_type = getattr(st, 'ExtensionLookupType', lookup.LookupType) if hasattr(actual, 'Format'): - _extract_pairpos_subtable(actual, glyph_to_cp, raw_kern) + # _extract_pairpos_subtable assumes a Type-2 (PairPos) + # subtable. Other lookup types reachable through the kern + # feature (cursive attachment, mark-to-mark, contextual, + # etc.) have a different shape and crash inside the + # extractor. Skip them with a debug note rather than + # aborting the whole build. Modern fonts often ship kern + # via Extension-wrapped PairPos, so checking the effective + # type instead of the outer type is what makes those + # lookups actually reach the extractor. + if effective_type == 2: + _extract_pairpos_subtable(actual, glyph_to_cp, raw_kern) + else: + print(f" Debug: skipping unsupported GPOS kern lookupType=" + f"{effective_type} (outer={lookup.LookupType}, Format={actual.Format})", + file=sys.stderr) font.close() @@ -425,11 +466,20 @@ def extract_ligatures_fonttools(font_path, codepoints): font.close() # Filter: only keep ligatures where all input and output codepoints are - # in our generated glyph set + # in our generated glyph set, and all codepoints fit in 16 bits. + # + # The on-disk format packs each component as a uint16 (the 3+ chained + # path packs `intermediate_cp << 16 | last_cp`, where `intermediate_cp` + # is the lig_cp of the prefix). Dropping any seq with an SMP cp here — + # plus any lig_cp > 0xFFFF — means every cp that reaches `packed = … << + # 16 | …` below is already 16-bit safe, including the chained path + # (intermediate_cp = filtered[prefix] is filtered too). codepoints_set = set(codepoints) filtered = {} for seq, lig_cp in raw_ligatures.items(): - if lig_cp not in codepoints_set: + if lig_cp not in codepoints_set or lig_cp > 0xFFFF: + continue + if any(cp > 0xFFFF for cp in seq): continue if all(cp in codepoints_set for cp in seq): filtered[seq] = lig_cp @@ -468,6 +518,12 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F style_label = style_names.get(style_id, str(style_id)) face = freetype.Face(fontfile) + # Set font size at 150 DPI (matching fontconvert.py) BEFORE any glyph load. + # load_glyph() with FT_LOAD_RENDER renders at the active size, so calling + # it before set_char_size() would waste work at the default size and risk + # Invalid_Size_Handle on some fonts. + face.set_char_size(size << 6, size << 6, 150, 150) + load_flags = freetype.FT_LOAD_RENDER if force_autohint: load_flags |= freetype.FT_LOAD_FORCE_AUTOHINT @@ -497,9 +553,6 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F total_glyphs = sum(end - start + 1 for start, end in intervals) print(f" [{style_label}] Validated: {len(intervals)} intervals, {total_glyphs} glyphs", file=sys.stderr) - # Set font size at 150 DPI (matching fontconvert.py) - face.set_char_size(size << 6, size << 6, 150, 150) - # Rasterize all glyphs total_bitmap_size = 0 all_glyphs = [] @@ -514,18 +567,28 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F bitmap = f.glyph.bitmap - # Build 4-bit greyscale bitmap (same logic as fontconvert.py) + # Build 4-bit greyscale bitmap (same logic as fontconvert.py). + # + # FreeType returns the buffer with bitmap.pitch as the row stride + # in bytes, which can be negative when the bitmap is stored + # bottom-up. Iterating bitmap.buffer linearly assumes + # pitch == width and a top-down layout — that holds in the common + # case but breaks on padded or flipped bitmaps and corrupts the + # output. Walk by (row, col) using the real pitch instead. pixels4g = [] px = 0 - for i, v in enumerate(bitmap.buffer): - x = i % bitmap.width - if x % 2 == 0: - px = (v >> 4) - else: - px = px | (v & 0xF0) - pixels4g.append(px) - px = 0 - if x == bitmap.width - 1 and bitmap.width % 2 > 0: + abs_pitch = abs(bitmap.pitch) + for y in range(bitmap.rows): + row_offset = y * abs_pitch if bitmap.pitch >= 0 else (bitmap.rows - 1 - y) * abs_pitch + for x in range(bitmap.width): + v = bitmap.buffer[row_offset + x] + if x % 2 == 0: + px = (v >> 4) + else: + px = px | (v & 0xF0) + pixels4g.append(px) + px = 0 + if bitmap.width % 2 > 0: pixels4g.append(px) px = 0 @@ -550,7 +613,12 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F pixels2b.append(px) px = 0 if (bitmap.width * bitmap.rows) % 4 != 0: - px = px << (4 - (bitmap.width * bitmap.rows) % 4) * 2 + # Outer parens are for clarity: in Python `*` binds tighter + # than `<<`, so the original `px << (4 - … % 4) * 2` already + # evaluates as `px << ((4 - … % 4) * 2)`. Match the explicit + # bracketing here so the shift width is obvious at a glance, + # mirroring the inner-loop style in fontconvert.py. + px = px << ((4 - (bitmap.width * bitmap.rows) % 4) * 2) pixels2b.append(px) packed = bytes(pixels2b) @@ -582,6 +650,10 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F all_cps = set(g.code_point for g, _ in all_glyphs) kern_map = extract_kerning_fonttools(fontfile, all_cps, ppem) + # SMP codepoints (> U+FFFF) cannot be stored in the uint16 kern codepoint + # field; drop them before class derivation to avoid a downstream + # struct.error when packing the binary kern tables. + kern_map = {(lcp, rcp): v for (lcp, rcp), v in kern_map.items() if lcp <= 0xFFFF and rcp <= 0xFFFF} print(f" [{style_label}] Kerning: {len(kern_map)} pairs extracted", file=sys.stderr) (kern_left_classes, kern_right_classes, kern_matrix, @@ -593,6 +665,9 @@ def rasterize_font_style(fontfile, size, intervals, style_id=0, force_autohint=F print(f" [{style_label}] Kerning classes: {kern_left_class_count} left, {kern_right_class_count} right, " f"{matrix_size + entries_size} bytes", file=sys.stderr) + # SMP codepoints in ligature inputs / outputs are filtered inside + # extract_ligatures_fonttools (see the codepoints_set filter), so every + # entry returned here is already 16-bit safe. ligature_pairs = extract_ligatures_fonttools(fontfile, all_cps) if len(ligature_pairs) > 255: print(f" [{style_label}] WARNING: {len(ligature_pairs)} ligature pairs exceeds uint8_t max (255), truncating", @@ -895,4 +970,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/lib/EpdFont/scripts/sd-fonts.yaml b/lib/EpdFont/scripts/sd-fonts.yaml index 6ba71c59..6ef1247c 100644 --- a/lib/EpdFont/scripts/sd-fonts.yaml +++ b/lib/EpdFont/scripts/sd-fonts.yaml @@ -108,9 +108,9 @@ families: intervals: latin-ext,cyrillic sizes: [12, 14, 16, 18] styles: - regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter%5Bwght%5D.ttf", variable: {wght: 400}} + regular: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter%5Bwght%5D.ttf", variable: {wght: 500}} bold: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter%5Bwght%5D.ttf", variable: {wght: 700}} - italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter-Italic%5Bwght%5D.ttf", variable: {wght: 400}} + italic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter-Italic%5Bwght%5D.ttf", variable: {wght: 500}} bolditalic: {url: "https://raw.githubusercontent.com/google/fonts/main/ofl/bitter/Bitter-Italic%5Bwght%5D.ttf", variable: {wght: 700}} # ── Sans-serif ───────────────────────────────────────────────────────── diff --git a/lib/I18n/translations/polish.yaml b/lib/I18n/translations/polish.yaml index 565fad83..b85a8719 100644 --- a/lib/I18n/translations/polish.yaml +++ b/lib/I18n/translations/polish.yaml @@ -38,7 +38,7 @@ STR_JOIN_NETWORK: "Dołącz do sieci" STR_CREATE_HOTSPOT: "Stwórz Hotspot" STR_JOIN_DESC: "Podłącz do istniejącej sieci WiFi" STR_HOTSPOT_DESC: "Stwórz sieć WiFi do której podłączyć mogą się inni" -STR_STARTING_HOTSPOT: "Startowanie Hotspot'a..." +STR_STARTING_HOTSPOT: "Startowanie Hotspota..." STR_HOTSPOT_MODE: "Tryb Hotspot" STR_CONNECT_WIFI_HINT: "Podłącz swoje urządzenie do tej sieci WiFi" STR_OPEN_URL_HINT: "Otwórz ten URL w przeglądarce" @@ -107,7 +107,7 @@ STR_KOREADER_AUTH: "KOReader Auth" STR_SYNC_READY: "KOReader sync gotowy" STR_AUTH_FAILED: "Błąd uwierzytelniania" STR_DONE: "Zrobione" -STR_CLEAR_CACHE_WARNING_1: "To wyczyści całą pamięc podręczną książek." +STR_CLEAR_CACHE_WARNING_1: "To wyczyści całą pamięć podręczną książek." STR_CLEAR_CACHE_WARNING_2: "Cały postęp czytania zostanie stracony!" STR_CLEAR_CACHE_WARNING_3: "Książki trzeba będzie ponownie indeksować" STR_CLEAR_CACHE_WARNING_4: "po ponownym otwarciu." @@ -130,6 +130,7 @@ STR_ALWAYS: "Zawsze" STR_IGNORE: "Ignoruj" STR_SLEEP: "Uśpienie" STR_PAGE_TURN: "Nast. str." +STR_FORCE_REFRESH: "Odśwież ekran" STR_PORTRAIT: "Pionowo" STR_LANDSCAPE_CW: "Poziomo P" STR_INVERTED: "Odwrócony" @@ -170,6 +171,7 @@ STR_NO_UPDATE: "Brak aktualizacji" STR_UPDATE_FAILED: "Aktualizacja nieudana" STR_UPDATE_COMPLETE: "Aktualizacja zakończona" STR_POWER_ON_HINT: "Przyciśnij i przytrzymaj przycisk zasilania aby włączyć ponownie" +STR_RESTARTING_HINT: "Restartowanie... Jeśli urządzenie się nie zrestartuje, przytrzymaj przycisk zasilania przez kilka sekund." STR_NO_ENTRIES: "Brak wpisów" STR_DOWNLOADING: "Pobieranie..." STR_DOWNLOAD_FAILED: "Błąd pobierania" @@ -178,6 +180,8 @@ STR_UNNAMED: "Nienazwany" STR_NO_SERVER_URL: "Brak skonfigurowanego serwera URL" STR_FETCH_FEED_FAILED: "Nie udało się pobrać kanału" STR_PARSE_FEED_FAILED: "Nie udało się przeanalizować kanału" +STR_NEXT_PAGE: "Następna strona »" +STR_PREV_PAGE: "« Poprzednia strona" STR_NETWORK_PREFIX: "Sieć: " STR_IP_ADDRESS_PREFIX: "Adres IP: " STR_ERROR_GENERAL_FAILURE: "Błąd: Ogólny" @@ -225,13 +229,18 @@ STR_EXAMPLE_BOOK: "Tytuł książki" STR_PREVIEW: "Podgląd" STR_TITLE: "Tytuł" STR_BATTERY: "Bateria" +STR_XTC_STATUS_BAR: "Pasek statusu XTC" +STR_BOTTOM: "Dół" +STR_TOP: "Góra" STR_UI_THEME: "Skórka UI" STR_THEME_CLASSIC: "Classic" STR_THEME_LYRA: "Lyra" +STR_THEME_ROUNDEDRAFF: "RoundedRaff" STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Przeciwdziałanie blaknięciu od słońca" STR_REMAP_FRONT_BUTTONS: "Skonfiguruj przednie przyciski" STR_OPDS_BROWSER: "OPDS Browser" +STR_SEARCH: "Szukaj" STR_COVER_CUSTOM: "Okładka + Własne" STR_MENU_RECENT_BOOKS: "Ostatnio czytane" STR_NO_RECENT_BOOKS: "Brak ostatnio czytanych" @@ -288,10 +297,64 @@ STR_UPLOAD: "Wyślij" STR_BOOK_S_STYLE: "Styl książki" STR_EMBEDDED_STYLE: "Style wbudowane w EPUB" STR_OPDS_SERVER_URL: "URL serwera OPDS" +STR_SET_SLEEP_COVER: "Ustaw okładkę" STR_FOOTNOTES: "Przypisy" STR_NO_FOOTNOTES: "Brak przypisów na tej stronie" STR_LINK: "[link]" STR_SCREENSHOT_BUTTON: "Zrób zrzut ekranu" +STR_ADD_SERVER: "Dodaj serwer" +STR_SERVER_NAME: "Nazwa serwera" +STR_NO_SERVERS: "Brak skonfigurowanych serwerów OPDS" +STR_DELETE_SERVER: "Usuń serwer" +STR_DELETE_CONFIRM: "Usunąć ten serwer?" +STR_OPDS_SERVERS: "Serwery OPDS" STR_AUTO_TURN_ENABLED: "Auto-kartkowanie: " STR_AUTO_TURN_PAGES_PER_MIN: "Auto-kartkowanie (str./min)" +STR_DOWNLOAD_FONTS: "Pobierz czcionki" +STR_FONT_DOWNLOAD: "Pobierz czcionkę" +STR_LOADING_FONT_LIST: "Ładowanie listy czcionek..." +STR_NO_FONTS_AVAILABLE: "Brak dostępnych czcionek" +STR_FONT_INSTALLED: "Czcionka zainstalowana!" +STR_FONT_INSTALL_FAILED: "Instalacja czcionki nieudana" +STR_INSTALLED: "Zainstalowany" +STR_CONFIRM_DOWNLOAD_PROMPT: "Pobrać?" +STR_SD_CARD_FULL: "Za mało pamięci na karcie SD" +STR_FILES_LABEL: "Pliki: " +STR_SIZE_LABEL: "Rozmiar: " +STR_REDOWNLOAD: "Re-download" +STR_DOWNLOAD_ALL: "Pobierz / Uaktualnij wszystkie" +STR_ALL_FONTS_INSTALLED: "Wszystkie czcionki zainstalowane!" +STR_UPDATE_AVAILABLE: "Uaktualnij" +STR_CRASH_TITLE: "Awaria systemu" +STR_CRASH_DESCRIPTION: "Szczegółowy raport zapisany do crash_report.txt. Prosimy o dołączenie tego pliku do zgłoszenia błędu." +STR_CRASH_REASON: "Powód awarii:" +STR_CRASH_NO_REASON: "(Nie odnotowano powodu)" STR_TILT_PAGE_TURN: "Kartkowanie przechyłem" +STR_KB_HINT_MOVE_CURSOR: "Naciśnij LEWO lub PRAWO aby poruszyć kursor" +STR_KB_HINT_RETURN_CURSOR: "Naciśnij LEWO aby powrócić do pozycji kursora" +STR_KB_HINT_HIDE_PASSWORD: "Przytrzymaj PRAWO potem naciśnij [***] aby ukryć hasło" +STR_KB_HINT_SHOW_PASSWORD: "Przytrzymaj PRAWO potem naciśnij [abc] aby pokazać hasło" +STR_KB_HINT_TOGGLE_HIDE_PASSWORD: "Naciśnij [***] aby ukryć hasło" +STR_KB_HINT_TOGGLE_SHOW_PASSWORD: "Naciśnij [abc] aby pokazać hasło" +STR_KB_HINT_EDIT_ENTRY: "Przytrzymaj GÓRA aby edytować wpis" +STR_KB_TIPS: "Porady:" +STR_KB_HINT_RETURN_KEYBOARD: "Naciśnij DÓŁ aby powrócić do klawiatury" +STR_KB_HINT_EXIT_URL_MODE: "Naciśnij ABC aby wyjść z trybu URL" +STR_KB_HINT_CLEAR_TEXT: "Przytrzymaj 'Backspace' aby wyczyścić cały tekst" +STR_KB_HINT_SECONDARY_CHAR: "Przytrz. 'Wybierz' dla znaku drugorzędnego" +STR_KB_HINT_UPPER_SECONDARY: "Przytrz. 'Wybierz' dla W. LITERY lub znaku drugorzędnego" +STR_KB_HINT_LOWER_SECONDARY: "Przytrz. 'Wybierz' dla m. litery lub znaku drugorzędnego" +STR_KB_HINT_URL_SNIPPETS: "Naciśnij URL dla adresów" +STR_SD_FIRMWARE_UPDATE: "Uaktualnienie oprogramowania z karty SD" +STR_SELECT_FIRMWARE_FILE: "Wybierz plik z oprogramowaniem (.bin)" +STR_NO_BIN_FILES: "Nie znaleziono plików .bin" +STR_VALIDATING_FIRMWARE: "Sprawdzanie oprogramowania..." +STR_INVALID_FIRMWARE: "Nieprawidłowy plik oprogramowania" +STR_FIRMWARE_TOO_LARGE: "Oprogramowanie za duże na partycję" +STR_FIRMWARE_TOO_SMALL: "Plik oprogramowania za mały" +STR_FIRMWARE_UPDATE_PROMPT: "Uaktualnić oprogramowanie?" +STR_FIRMWARE_FILE_OPEN_FAILED: "Nie można otworzyć pliku" +STR_FIRMWARE_WRITE_FAILED: "Zapis oprogramowania nieudany" +STR_FIRMWARE_UPDATE_DO_NOT_POWER_OFF: "Nie wyłączać!" +STR_RECOVERY_MODE: "Tryb przywracania" +STR_RECOVERY_MODE_HINT: "Umieść firmware.bin w głównym katalogu karty SD i wybierz go" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index 2505c935..84ac108e 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -229,6 +229,9 @@ STR_EXAMPLE_BOOK: "Boktitel" STR_PREVIEW: "Förhandsgranskning" STR_TITLE: "Titel" STR_BATTERY: "Batteri" +STR_XTC_STATUS_BAR: "XTC-statusfält" +STR_BOTTOM: "Botten" +STR_TOP: "Överst" STR_UI_THEME: "Användargränssnittstema" STR_THEME_CLASSIC: "Klassisk" STR_THEME_LYRA: "Lyra" @@ -294,6 +297,7 @@ STR_UPLOAD: "Uppladdning" STR_BOOK_S_STYLE: "Bokstil" STR_EMBEDDED_STYLE: "Inbäddad stil" STR_OPDS_SERVER_URL: "OPDS-serveradress" +STR_SET_SLEEP_COVER: "Ställ in omslag" STR_FOOTNOTES: "Fotnoter" STR_NO_FOOTNOTES: "Inga fotnoter på den här sidan" STR_LINK: "[länk]" @@ -306,6 +310,21 @@ STR_DELETE_CONFIRM: "Vill du ta bort den här servern?" STR_OPDS_SERVERS: "OPDS-servrar" STR_AUTO_TURN_ENABLED: "Automatisk vändning aktiverad: " STR_AUTO_TURN_PAGES_PER_MIN: "Automatisk vändning (sidor per minut)" +STR_DOWNLOAD_FONTS: "Ladda ner teckensnitt" +STR_FONT_DOWNLOAD: "Nedladdning av teckensnitt" +STR_LOADING_FONT_LIST: "Laddar teckensnittslista..." +STR_NO_FONTS_AVAILABLE: "Inga teckensnitt tillgängliga" +STR_FONT_INSTALLED: "Teckensnitt installerat!" +STR_FONT_INSTALL_FAILED: "Installationen av teckensnitt misslyckades" +STR_INSTALLED: "Installerad" +STR_CONFIRM_DOWNLOAD_PROMPT: "Nedladdning?" +STR_SD_CARD_FULL: "Otillräckligt utrymme på SD-kortet" +STR_FILES_LABEL: "Filer: " +STR_SIZE_LABEL: "Storlek: " +STR_REDOWNLOAD: "Ladda ner igen" +STR_DOWNLOAD_ALL: "Ladda ner / Uppdatera alla" +STR_ALL_FONTS_INSTALLED: "Alla teckensnitt installerade!" +STR_UPDATE_AVAILABLE: "Uppdatering" STR_CRASH_TITLE: "Systemkrasch" STR_CRASH_DESCRIPTION: "En detaljerad rapport sparades till crash_report.txt. Vänligen inkludera den här filen i din felrapport." STR_CRASH_REASON: "Orsak till kraschen:" diff --git a/scripts/generate-font-manifest.py b/scripts/generate-font-manifest.py index 695bb99b..e962c162 100755 --- a/scripts/generate-font-manifest.py +++ b/scripts/generate-font-manifest.py @@ -21,6 +21,7 @@ import json import os import struct import sys +import zlib from pathlib import Path # Import canonical version constants from the shared file in lib/EpdFont/scripts/ @@ -115,6 +116,15 @@ def parse_filename(filename: str) -> tuple[str, str] | None: return family, size_str +def compute_crc32(filepath: Path) -> int: + """Compute CRC32 of a file, matching esp_rom_crc32_le(0xFFFFFFFF, ...) ^ 0xFFFFFFFF.""" + crc = 0 + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + crc = zlib.crc32(chunk, crc) + return crc & 0xFFFFFFFF + + def scan_cpfont_files(input_dir: Path) -> dict[str, list[Path]]: """Scan input directory for .cpfont files, grouped by family name. @@ -164,6 +174,7 @@ def build_manifest( { "name": filepath.name, "size": filepath.stat().st_size, + "crc32": compute_crc32(filepath), } ) diff --git a/src/SdCardFontSystem.cpp b/src/SdCardFontSystem.cpp index b771bede..8e59d6ed 100644 --- a/src/SdCardFontSystem.cpp +++ b/src/SdCardFontSystem.cpp @@ -5,10 +5,15 @@ #include "CrossPointSettings.h" -static uint8_t fontSizeEnumFromSettings() { +// Map fontSize enum (SMALL=0, MEDIUM=1, LARGE=2, EXTRA_LARGE=3) to the point +// sizes shipped with the built-in fonts. Used to drive closest-pt selection +// in the SD card font registry (see SdCardFontFamilyInfo::pickClosestSize). +static constexpr uint8_t FONT_SIZE_TO_PT[CrossPointSettings::FONT_SIZE_COUNT] = {12, 14, 16, 18}; + +static uint8_t targetPtSizeFromSettings() { uint8_t e = SETTINGS.fontSize; if (e >= CrossPointSettings::FONT_SIZE_COUNT) e = 1; // default to MEDIUM - return e; + return FONT_SIZE_TO_PT[e]; } void SdCardFontSystem::begin(GfxRenderer& renderer) { @@ -25,7 +30,7 @@ void SdCardFontSystem::begin(GfxRenderer& renderer) { if (SETTINGS.sdFontFamilyName[0] != '\0') { const auto* family = registry_.findFamily(SETTINGS.sdFontFamilyName); if (family) { - if (manager_.loadFamily(*family, renderer, fontSizeEnumFromSettings())) { + if (manager_.loadFamily(*family, renderer, targetPtSizeFromSettings())) { LOG_DBG("SDFS", "Loaded SD card font family: %s", SETTINGS.sdFontFamilyName); } else { LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", SETTINGS.sdFontFamilyName); @@ -53,7 +58,7 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) { const char* wantedFamily = SETTINGS.sdFontFamilyName; const std::string& currentFamily = manager_.currentFamilyName(); - const uint8_t sizeEnum = fontSizeEnumFromSettings(); + const uint8_t targetPt = targetPtSizeFromSettings(); if (wantedFamily[0] == '\0') { if (!currentFamily.empty()) { @@ -62,8 +67,8 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) { return; } - // Reload if family changed OR if the user-selected size maps to a - // different file than what's currently loaded OR if the registry was + // Reload if family changed OR if the user-selected size now resolves to a + // different on-disk file than what's currently loaded OR if the registry was // just rediscovered (file may have been replaced on disk). bool familyMatches = (currentFamily == wantedFamily); if (familyMatches) { @@ -74,13 +79,11 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) { SETTINGS.sdFontFamilyName[0] = '\0'; return; } - auto sizes = family->availableSizes(); - uint8_t idx = sizeEnum; - if (idx >= sizes.size()) idx = sizes.size() - 1; - uint8_t wantedPt = sizes.empty() ? 0 : sizes[idx]; - if (!registryWasDirty && wantedPt == manager_.currentPointSize()) return; - LOG_DBG("SDFS", "Reloading %s: size %u -> %u (enum %u)%s", wantedFamily, manager_.currentPointSize(), wantedPt, - sizeEnum, registryWasDirty ? " [registry dirty]" : ""); + const auto* best = family->pickClosestSize(targetPt); + const uint8_t bestPt = best ? best->pointSize : 0; + if (!registryWasDirty && bestPt == manager_.currentPointSize()) return; + LOG_DBG("SDFS", "Reloading %s: size %u -> %u (target %u)%s", wantedFamily, manager_.currentPointSize(), bestPt, + targetPt, registryWasDirty ? " [registry dirty]" : ""); } if (!currentFamily.empty()) { @@ -89,7 +92,7 @@ void SdCardFontSystem::ensureLoaded(GfxRenderer& renderer) { const auto* family = registry_.findFamily(wantedFamily); if (family) { - if (manager_.loadFamily(*family, renderer, sizeEnum)) { + if (manager_.loadFamily(*family, renderer, targetPt)) { LOG_DBG("SDFS", "Loaded SD font family: %s", wantedFamily); } else { LOG_ERR("SDFS", "Failed to load SD font family: %s (clearing)", wantedFamily); diff --git a/src/activities/settings/FontDownloadActivity.cpp b/src/activities/settings/FontDownloadActivity.cpp index e7419c0e..ce18ce67 100644 --- a/src/activities/settings/FontDownloadActivity.cpp +++ b/src/activities/settings/FontDownloadActivity.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "MappedInputManager.h" #include "SdCardFontGlobals.h" @@ -123,6 +124,14 @@ bool FontDownloadActivity::fetchAndParseManifest() { ManifestFile file; file.name = fileObj["name"] | ""; file.size = fileObj["size"] | 0; + + if (!fileObj["crc32"].is()) { + LOG_ERR("FONT", "Malformed manifest file entry: missing or invalid crc32 for %s", file.name.c_str()); + errorMessage_ = "Invalid font manifest"; + return false; + } + file.crc32 = fileObj["crc32"].as(); + family.totalSize += file.size; family.files.push_back(std::move(file)); } @@ -181,6 +190,24 @@ size_t FontDownloadActivity::totalUninstalledSize() const { return total; } +// Standard CRC32 matching zlib/Python zlib.crc32(). +bool FontDownloadActivity::computeFileCrc32(const char* path, uint32_t& outCrc) { + FsFile f; + if (!Storage.openFileForRead("FONT", path, f)) { + return false; + } + constexpr size_t BUF_SIZE = 128; + uint8_t buf[BUF_SIZE]; + uint32_t crc = 0; + while (f.available()) { + const int n = f.read(buf, BUF_SIZE); + if (n <= 0) break; + crc = esp_rom_crc32_le(crc, buf, static_cast(n)); + } + outCrc = crc; + return true; +} + void FontDownloadActivity::downloadFamily(ManifestFamily& family) { { RenderLock lock(*this); @@ -233,6 +260,29 @@ void FontDownloadActivity::downloadFamily(ManifestFamily& family) { return; } + uint32_t actualCrc = 0; + if (!computeFileCrc32(destPath, actualCrc)) { + LOG_ERR("FONT", "Failed to open file for CRC check: %s", destPath); + fontInstaller_.deleteFamily(family.name.c_str()); + family.installed = false; + family.hasUpdate = false; + RenderLock lock(*this); + state_ = ERROR; + errorMessage_ = "Failed to compute checksum: " + file.name; + return; + } + if (actualCrc != file.crc32) { + LOG_ERR("FONT", "CRC32 mismatch for %s: got %08x expected %08x", file.name.c_str(), actualCrc, file.crc32); + fontInstaller_.deleteFamily(family.name.c_str()); + family.installed = false; + family.hasUpdate = false; + RenderLock lock(*this); + state_ = ERROR; + errorMessage_ = "Checksum mismatch: " + file.name; + return; + } + LOG_DBG("FONT", "Downloaded %s (size=%zu crc32=%08x)", file.name.c_str(), file.size, actualCrc); + if (!fontInstaller_.validateCpfontFile(destPath)) { LOG_ERR("FONT", "Invalid .cpfont: %s", destPath); fontInstaller_.deleteFamily(family.name.c_str()); diff --git a/src/activities/settings/FontDownloadActivity.h b/src/activities/settings/FontDownloadActivity.h index 1b4373c4..16d52fa9 100644 --- a/src/activities/settings/FontDownloadActivity.h +++ b/src/activities/settings/FontDownloadActivity.h @@ -50,6 +50,7 @@ class FontDownloadActivity : public Activity { struct ManifestFile { std::string name; size_t size = 0; + uint32_t crc32 = 0; }; struct ManifestFamily { @@ -83,6 +84,7 @@ class FontDownloadActivity : public Activity { bool fetchAndParseManifest(); void downloadFamily(ManifestFamily& family); void downloadAll(); + static bool computeFileCrc32(const char* path, uint32_t& outCrc); bool isDownloadAllSelected() const { return selectedIndex_ == 0 && !families_.empty(); } int familyIndexFromList(int listIndex) const { return listIndex - 1; } int listItemCount() const { return families_.empty() ? 0 : static_cast(families_.size()) + 1; } diff --git a/src/components/themes/roundedraff/RoundedRaffTheme.cpp b/src/components/themes/roundedraff/RoundedRaffTheme.cpp index 6d80e8f3..3e0ede09 100644 --- a/src/components/themes/roundedraff/RoundedRaffTheme.cpp +++ b/src/components/themes/roundedraff/RoundedRaffTheme.cpp @@ -122,7 +122,7 @@ void RoundedRaffTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const } // Full-width divider between tabs and setting rows. - renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width, rect.y + rect.height - 1, true); + renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width - 1, rect.y + rect.height - 1, true); } void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector& recentBooks, @@ -243,6 +243,77 @@ void RoundedRaffTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int butt drawScrollBar(renderer, rect, buttonCount, pageStartIndex, pageItems); } +void RoundedRaffTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode, + int contentStartX, int contentWidth) const { + const auto& metrics = UITheme::getInstance().getMetrics(); + const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); + const int lineY = rect.y + rect.height + lineHeight + metrics.verticalSpacing; + const int thickness = cursorMode ? 3 : 2; + + if (contentWidth > 0) { + renderer.drawLine(rect.x + contentStartX, lineY, rect.x + contentStartX + contentWidth - 1, lineY, thickness, true); + return; + } + + constexpr int hPadding = 8; + const int lineW = textWidth + hPadding * 2; + const int lineStart = rect.x + (rect.width - lineW) / 2; + renderer.drawLine(lineStart, lineY, lineStart + lineW - 1, lineY, thickness, true); +} + +void RoundedRaffTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected, + const char* secondaryLabel, const KeyboardKeyType keyType, + const bool inactiveSelection) const { + constexpr int keyRadius = 10; + const bool disabled = keyType == KeyboardKeyType::Disabled; + const bool invert = isSelected && !inactiveSelection; + + if (isSelected) { + const Color fillColor = (inactiveSelection || disabled) ? Color::LightGray : Color::Black; + renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, keyRadius, fillColor); + } else { + if (disabled) { + renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, keyRadius, Color::LightGray); + } else { + renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, keyRadius, Color::White); + } + renderer.drawRoundedRect(rect.x, rect.y, rect.width, rect.height, 1, keyRadius, true); + } + + if (keyType == KeyboardKeyType::Space) { + const int lineHalfWidth = rect.width * 3 / 10; + const int centerX = rect.x + rect.width / 2; + const int lineY = rect.y + rect.height / 2 + 3; + renderer.drawLine(centerX - lineHalfWidth, lineY, centerX + lineHalfWidth, lineY, 3, !invert); + return; + } + + if (keyType == KeyboardKeyType::Del) { + const int centerX = rect.x + rect.width / 2; + const int centerY = rect.y + rect.height / 2; + const int arrowLen = rect.width / 4; + const int arrowHead = std::max(1, arrowLen / 2); + renderer.drawLine(centerX - arrowLen / 2, centerY, centerX + arrowLen / 2, centerY, 3, !invert); + renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY - arrowHead, 3, + !invert); + renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY + arrowHead, 3, + !invert); + return; + } + + if (label != nullptr && label[0] != '\0') { + const int itemWidth = renderer.getTextWidth(UI_12_FONT_ID, label); + const int textX = rect.x + (rect.width - itemWidth) / 2; + const int textY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2; + renderer.drawText(UI_12_FONT_ID, textX, textY, label, !invert); + } + + if (secondaryLabel != nullptr && secondaryLabel[0] != '\0') { + const int secWidth = renderer.getTextWidth(SMALL_FONT_ID, secondaryLabel); + renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - secWidth - 3, rect.y + 1, secondaryLabel, !invert); + } +} + void RoundedRaffTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, const std::function& rowTitle, const std::function& rowSubtitle, diff --git a/src/components/themes/roundedraff/RoundedRaffTheme.h b/src/components/themes/roundedraff/RoundedRaffTheme.h index 5a03a037..b73a5b55 100644 --- a/src/components/themes/roundedraff/RoundedRaffTheme.h +++ b/src/components/themes/roundedraff/RoundedRaffTheme.h @@ -36,8 +36,14 @@ constexpr ThemeMetrics values = {.batteryWidth = 15, .keyboardKeyWidth = 22, .keyboardKeyHeight = 30, .keyboardKeySpacing = 10, - .keyboardBottomAligned = false, - .keyboardCenteredText = false}; + .keyboardBottomKeyHeight = 30, + .keyboardBottomKeySpacing = 5, + .keyboardBottomAligned = true, + .keyboardCenteredText = false, + .keyboardVerticalOffset = 0, + .keyboardTextFieldWidthPercent = 85, + .keyboardWidthPercent = 90, + .keyboardKeyCornerRadius = 0}; } class RoundedRaffTheme : public BaseTheme { @@ -52,6 +58,11 @@ class RoundedRaffTheme : public BaseTheme { void drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex, const std::function& buttonLabel, const std::function& rowIcon) const override; + void drawTextField(const GfxRenderer& renderer, Rect rect, int textWidth, bool cursorMode = false, + int contentStartX = 0, int contentWidth = 0) const override; + void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, bool isSelected, + const char* secondaryLabel = nullptr, KeyboardKeyType keyType = KeyboardKeyType::Normal, + bool inactiveSelection = false) const override; void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, const std::function& rowTitle, const std::function& rowSubtitle = nullptr,