diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index db725320..79b3829b 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -19,6 +19,70 @@ constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading // Cache file magic and version constexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI" constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes + +// Find UTF-8 character boundary at or before pos +size_t findUtf8Boundary(const std::string& str, size_t pos) { + if (pos >= str.length()) return str.length(); + // Move back if we're in the middle of a UTF-8 sequence + while (pos > 0 && (str[pos] & 0xC0) == 0x80) { + pos--; + } + return pos; +} + +// Binary search to find max characters that fit in width +// Returns the position (byte offset) where to break the string +size_t findBreakPosition(GfxRenderer& renderer, int fontId, const std::string& line, int maxWidth) { + if (line.empty()) return 0; + + // First check if the whole line fits + int fullWidth = renderer.getTextWidth(fontId, line.c_str()); + if (fullWidth <= maxWidth) { + return line.length(); + } + + // Binary search for the break point + size_t low = 1; // At minimum 1 character + size_t high = line.length(); + size_t bestFit = 1; + + while (low < high) { + size_t mid = (low + high + 1) / 2; + mid = findUtf8Boundary(line, mid); + + if (mid <= low) { + // Can't make progress, exit + break; + } + + std::string substr = line.substr(0, mid); + int width = renderer.getTextWidth(fontId, substr.c_str()); + + if (width <= maxWidth) { + bestFit = mid; + low = mid; + } else { + high = mid - 1; + if (high > 0) { + high = findUtf8Boundary(line, high); + } + } + } + + // Try to break at word boundary (space) if possible + if (bestFit > 0 && bestFit < line.length()) { + size_t spacePos = line.rfind(' ', bestFit); + if (spacePos != std::string::npos && spacePos > 0) { + // Check if breaking at space still fits + std::string atSpace = line.substr(0, spacePos); + if (renderer.getTextWidth(fontId, atSpace.c_str()) <= maxWidth) { + return spacePos; + } + } + } + + return bestFit > 0 ? bestFit : 1; // At minimum, consume 1 character +} } // namespace void TxtReaderActivity::taskTrampoline(void* param) { @@ -303,36 +367,21 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector // Track position within this source line (in bytes from pos) size_t lineBytePos = 0; - // Word wrap if needed + // Word wrap if needed - use binary search for performance with SD fonts while (!line.empty() && static_cast(outLines.size()) < linesPerPage) { - int lineWidth = renderer.getTextWidth(cachedFontId, line.c_str()); + // Use binary search to find break position (much faster than linear search) + size_t breakPos = findBreakPosition(renderer, cachedFontId, line, viewportWidth); - if (lineWidth <= viewportWidth) { + if (breakPos >= line.length()) { + // Whole line fits outLines.push_back(line); - lineBytePos = displayLen; // Consumed entire display content + lineBytePos = displayLen; line.clear(); break; } - // Find break point - size_t breakPos = line.length(); - while (breakPos > 0 && renderer.getTextWidth(cachedFontId, line.substr(0, breakPos).c_str()) > viewportWidth) { - // Try to break at space - size_t spacePos = line.rfind(' ', breakPos - 1); - if (spacePos != std::string::npos && spacePos > 0) { - breakPos = spacePos; - } else { - // Break at character boundary for UTF-8 - breakPos--; - // Make sure we don't break in the middle of a UTF-8 sequence - while (breakPos > 0 && (line[breakPos] & 0xC0) == 0x80) { - breakPos--; - } - } - } - if (breakPos == 0) { - breakPos = 1; + breakPos = 1; // Ensure progress } outLines.push_back(line.substr(0, breakPos));