diff --git a/lib/ThemeEngine/include/ThemeManager.h b/lib/ThemeEngine/include/ThemeManager.h index 6f143ac4..f028c72d 100644 --- a/lib/ThemeEngine/include/ThemeManager.h +++ b/lib/ThemeEngine/include/ThemeManager.h @@ -130,6 +130,7 @@ class ThemeManager { // Asset caching const std::vector* getCachedAsset(const std::string& path); + void cacheAsset(const std::string& path, std::vector&& data); const ProcessedAsset* getProcessedAsset(const std::string& path, GfxRenderer::Orientation orientation, int targetW = 0, int targetH = 0); void cacheProcessedAsset(const std::string& path, const ProcessedAsset& asset, int targetW = 0, int targetH = 0); diff --git a/lib/ThemeEngine/include/UIElement.h b/lib/ThemeEngine/include/UIElement.h index fd3a34f6..be7db4b8 100644 --- a/lib/ThemeEngine/include/UIElement.h +++ b/lib/ThemeEngine/include/UIElement.h @@ -29,6 +29,10 @@ class UIElement { // Recomputed every layout pass int absX = 0, absY = 0, absW = 0, absH = 0; + // Layout caching - track last params to skip redundant layout + int lastParentX = -1, lastParentY = -1, lastParentW = -1, lastParentH = -1; + bool layoutValid = false; + // Caching support bool cacheable = false; // Set true for expensive elements like bitmaps bool cacheValid = false; // Is the cached render still valid? @@ -84,6 +88,7 @@ class UIElement { virtual void markDirty() { dirty = true; cacheValid = false; + layoutValid = false; } void markClean() { dirty = false; } @@ -97,6 +102,18 @@ class UIElement { // Calculate absolute position based on parent virtual void layout(const ThemeContext& context, int parentX, int parentY, int parentW, int parentH) { + // Skip layout if params unchanged and layout is still valid + if (layoutValid && parentX == lastParentX && parentY == lastParentY && parentW == lastParentW && + parentH == lastParentH) { + return; + } + + lastParentX = parentX; + lastParentY = parentY; + lastParentW = parentW; + lastParentH = parentH; + layoutValid = true; + int newX = parentX + x.resolve(parentW); int newY = parentY + y.resolve(parentH); int newW = width.resolve(parentW); diff --git a/lib/ThemeEngine/src/BasicElements.cpp b/lib/ThemeEngine/src/BasicElements.cpp index a9ef60bb..d1f0b679 100644 --- a/lib/ThemeEngine/src/BasicElements.cpp +++ b/lib/ThemeEngine/src/BasicElements.cpp @@ -137,38 +137,18 @@ void Label::draw(const GfxRenderer& renderer, const ThemeContext& context) { break; } - // Binary search for cut point + // Binary search for maximum characters that fit (O(log n) instead of O(n)) int len = remaining.length(); - int cut = len; - - // Find split point - // Optimistic start: approximate chars that fit - int avgCharWidth = renderer.getTextWidth(fontId, "a"); - if (avgCharWidth < 1) avgCharWidth = 8; - int approxChars = absW / avgCharWidth; - if (approxChars < 1) approxChars = 1; - if (approxChars >= len) approxChars = len - 1; - - // Refine from approxChars - int w = renderer.getTextWidth(fontId, remaining.substr(0, approxChars).c_str()); - if (w < absW) { - // Grow - for (int i = approxChars; i <= len; i++) { - if (renderer.getTextWidth(fontId, remaining.substr(0, i).c_str()) > absW) { - cut = i - 1; - break; - } - cut = i; - } - } else { - // Shrink - for (int i = approxChars; i > 0; i--) { - if (renderer.getTextWidth(fontId, remaining.substr(0, i).c_str()) <= absW) { - cut = i; - break; - } + int lo = 1, hi = len; + while (lo < hi) { + int mid = (lo + hi + 1) / 2; + if (renderer.getTextWidth(fontId, remaining.substr(0, mid).c_str()) <= absW) { + lo = mid; + } else { + hi = mid - 1; } } + int cut = lo; // Find last space before cut if (cut < (int)remaining.length()) { @@ -249,97 +229,87 @@ void BitmapElement::draw(const GfxRenderer& renderer, const ThemeContext& contex return; } - // Resolve simplified or relative paths if (path.find('/') == std::string::npos || (path.length() > 0 && path[0] != '/')) { path = ThemeManager::get().getAssetPath(path); } - // 1. Check if we have a cached 1-bit render + // Fast path: use cached 1-bit render const ProcessedAsset* processed = ThemeManager::get().getProcessedAsset(path, renderer.getOrientation(), absW, absH); if (processed && processed->w == absW && processed->h == absH) { - const int rowBytes = (absW + 7) / 8; - for (int y = 0; y < absH; y++) { - const uint8_t* srcRow = processed->data.data() + y * rowBytes; - for (int x = 0; x < absW; x++) { - // Cached 1-bit data: 0=Black, 1=White - bool isBlack = !(srcRow[x / 8] & (1 << (7 - (x % 8)))); - // Draw opaque (true=black, false=white) - renderer.drawPixel(absX + x, absY + y, isBlack); - } - } + renderer.restoreRegion(processed->data.data(), absX, absY, absW, absH); markClean(); return; } + // Helper to draw bitmap with centering and optional rounded corners + auto drawBmp = [&](Bitmap& bmp) { + int drawX = absX; + int drawY = absY; + if (bmp.getWidth() < absW) drawX += (absW - bmp.getWidth()) / 2; + if (bmp.getHeight() < absH) drawY += (absH - bmp.getHeight()) / 2; + if (borderRadius > 0) { + renderer.drawRoundedBitmap(bmp, drawX, drawY, absW, absH, borderRadius); + } else { + renderer.drawBitmap(bmp, drawX, drawY, absW, absH); + } + }; + bool drawSuccess = false; - // 2. Try Streaming (Absolute paths, large images) - if (path.length() > 0 && path[0] == '/') { + // Try RAM cache first + const std::vector* cachedData = ThemeManager::get().getCachedAsset(path); + if (cachedData && !cachedData->empty()) { + Bitmap bmp(cachedData->data(), cachedData->size()); + if (bmp.parseHeaders() == BmpReaderError::Ok) { + drawBmp(bmp); + drawSuccess = true; + } + } + + // Fallback: load from SD card + if (!drawSuccess && path.length() > 0 && path[0] == '/') { FsFile file; if (SdMan.openFileForRead("HOME", path, file)) { - Bitmap bmp(file, true); // (file, dithering=true) - if (bmp.parseHeaders() == BmpReaderError::Ok) { - int drawX = absX; - int drawY = absY; - if (bmp.getWidth() < absW) drawX += (absW - bmp.getWidth()) / 2; - if (bmp.getHeight() < absH) drawY += (absH - bmp.getHeight()) / 2; - - if (borderRadius > 0) { - renderer.drawRoundedBitmap(bmp, drawX, drawY, absW, absH, borderRadius); - } else { - renderer.drawBitmap(bmp, drawX, drawY, absW, absH); + size_t fileSize = file.size(); + if (fileSize > 0 && fileSize < 100000) { + std::vector fileData(fileSize); + if (file.read(fileData.data(), fileSize) == fileSize) { + ThemeManager::get().cacheAsset(path, std::move(fileData)); + const std::vector* newCachedData = ThemeManager::get().getCachedAsset(path); + if (newCachedData && !newCachedData->empty()) { + Bitmap bmp(newCachedData->data(), newCachedData->size()); + if (bmp.parseHeaders() == BmpReaderError::Ok) { + drawBmp(bmp); + drawSuccess = true; + } + } + } + } else { + Bitmap bmp(file, true); + if (bmp.parseHeaders() == BmpReaderError::Ok) { + drawBmp(bmp); + drawSuccess = true; } - drawSuccess = true; } file.close(); } } - // 3. Fallback to RAM Cache (Standard method) - if (!drawSuccess) { - const std::vector* data = ThemeManager::get().getCachedAsset(path); - if (data && !data->empty()) { - Bitmap bmp(data->data(), data->size()); - if (bmp.parseHeaders() == BmpReaderError::Ok) { - int drawX = absX; - int drawY = absY; - if (bmp.getWidth() < absW) drawX += (absW - bmp.getWidth()) / 2; - if (bmp.getHeight() < absH) drawY += (absH - bmp.getHeight()) / 2; - - if (borderRadius > 0) { - renderer.drawRoundedBitmap(bmp, drawX, drawY, absW, absH, borderRadius); - } else { - renderer.drawBitmap(bmp, drawX, drawY, absW, absH); - } - drawSuccess = true; - } + // Cache rendered result for fast subsequent draws using captureRegion + if (drawSuccess && absW * absH <= 40000) { + size_t capturedSize = 0; + uint8_t* captured = renderer.captureRegion(absX, absY, absW, absH, &capturedSize); + if (captured && capturedSize > 0) { + ProcessedAsset asset; + asset.w = absW; + asset.h = absH; + asset.orientation = renderer.getOrientation(); + asset.data.assign(captured, captured + capturedSize); + free(captured); + ThemeManager::get().cacheProcessedAsset(path, asset, absW, absH); } } - // 4. Cache result if successful - if (drawSuccess) { - ProcessedAsset asset; - asset.w = absW; - asset.h = absH; - asset.orientation = renderer.getOrientation(); - - const int rowBytes = (absW + 7) / 8; - asset.data.resize(rowBytes * absH, 0xFF); // Initialize to 0xFF (White) - - for (int y = 0; y < absH; y++) { - uint8_t* dstRow = asset.data.data() + y * rowBytes; - for (int x = 0; x < absW; x++) { - // Read precise pixel state from framebuffer - bool isBlack = renderer.readPixel(absX + x, absY + y); - if (isBlack) { - // Clear bit for black (0) - dstRow[x / 8] &= ~(1 << (7 - (x % 8))); - } - } - } - ThemeManager::get().cacheProcessedAsset(path, asset, absW, absH); - } - markClean(); } @@ -421,12 +391,6 @@ void List::draw(const GfxRenderer& renderer, const ThemeContext& context) { itemContext.setString("Item.Icon", context.getString(prefix + "Icon")); itemContext.setString("Item.Image", context.getString(prefix + "Image")); itemContext.setString("Item.Progress", context.getString(prefix + "Progress")); - itemContext.setInt("Item.Index", i); - itemContext.setInt("Item.Count", count); - // ValueIndex may not exist for all item types, so check first - if (context.hasKey(prefix + "ValueIndex")) { - itemContext.setInt("Item.ValueIndex", context.getInt(prefix + "ValueIndex")); - } // Viewport check if (direction == Direction::Horizontal) { @@ -449,6 +413,12 @@ void List::draw(const GfxRenderer& renderer, const ThemeContext& context) { } if (currentY > absY + absH) break; } + itemContext.setInt("Item.Index", i); + itemContext.setInt("Item.Count", count); + // ValueIndex may not exist for all item types, so check first + if (context.hasKey(prefix + "ValueIndex")) { + itemContext.setInt("Item.ValueIndex", context.getInt(prefix + "ValueIndex")); + } // Layout and draw itemTemplate->layout(itemContext, currentX, currentY, itemW, itemH); diff --git a/lib/ThemeEngine/src/ThemeManager.cpp b/lib/ThemeEngine/src/ThemeManager.cpp index 2902f5b2..a3ba9314 100644 --- a/lib/ThemeEngine/src/ThemeManager.cpp +++ b/lib/ThemeEngine/src/ThemeManager.cpp @@ -396,6 +396,10 @@ const std::vector* ThemeManager::getCachedAsset(const std::string& path return nullptr; } +void ThemeManager::cacheAsset(const std::string& path, std::vector&& data) { + assetCache[path] = std::move(data); +} + const ProcessedAsset* ThemeManager::getProcessedAsset(const std::string& path, GfxRenderer::Orientation orientation, int targetW, int targetH) { std::string cacheKey = path; @@ -597,7 +601,23 @@ void ThemeManager::renderScreen(const std::string& screenName, const GfxRenderer void ThemeManager::renderScreenOptimized(const std::string& screenName, const GfxRenderer& renderer, const ThemeContext& context, const ThemeContext* prevContext) { - renderScreen(screenName, renderer, context); + if (elements.count(screenName) == 0) { + return; + } + + UIElement* root = elements[screenName]; + + // Layout uses internal caching - will skip if params unchanged + root->layout(context, 0, 0, renderer.getScreenWidth(), renderer.getScreenHeight()); + + // If no previous context provided, do full draw + if (!prevContext) { + root->draw(renderer, context); + return; + } + + // Draw elements - dirty tracking is handled internally by each element + root->draw(renderer, context); } } // namespace ThemeEngine diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 5f3fb2e0..77d3ae84 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -47,51 +47,93 @@ void HomeActivity::onEnter() { // Check if OPDS browser URL is configured hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0; + // Load and cache recent books data FIRST (loads each book only once) + loadRecentBooksData(); + if (hasContinueReading) { - // Extract filename from path for display - lastBookTitle = APP_STATE.openEpubPath; - const size_t lastSlash = lastBookTitle.find_last_of('/'); - if (lastSlash != std::string::npos) { - lastBookTitle = lastBookTitle.substr(lastSlash + 1); + // Initialize defaults + cachedChapterTitle = ""; + cachedCurrentPage = "-"; + cachedTotalPages = "-"; + cachedProgressPercent = 0; + + // Check if current book is in recent books - use cached data instead of reloading + bool foundInRecent = false; + for (const auto& book : cachedRecentBooks) { + if (book.path == APP_STATE.openEpubPath) { + lastBookTitle = book.title; + coverBmpPath = book.coverPath; + hasCoverImage = !book.coverPath.empty(); + cachedProgressPercent = book.progressPercent; + foundInRecent = true; + break; + } } - // If epub, try to load the metadata for title/author and cover - if (StringUtils::checkFileExtension(lastBookTitle, ".epub")) { - Epub epub(APP_STATE.openEpubPath, "/.crosspoint"); - epub.load(false); - if (!epub.getTitle().empty()) { - lastBookTitle = std::string(epub.getTitle()); + if (!foundInRecent) { + // Book not in recent list, need to load it + lastBookTitle = APP_STATE.openEpubPath; + const size_t lastSlash = lastBookTitle.find_last_of('/'); + if (lastSlash != std::string::npos) { + lastBookTitle = lastBookTitle.substr(lastSlash + 1); } - if (!epub.getAuthor().empty()) { - lastBookAuthor = std::string(epub.getAuthor()); - } - // Try to generate thumbnail image for Continue Reading card - if (epub.generateThumbBmp()) { - coverBmpPath = epub.getThumbBmpPath(); - hasCoverImage = true; - } - } else if (StringUtils::checkFileExtension(lastBookTitle, ".xtch") || - StringUtils::checkFileExtension(lastBookTitle, ".xtc")) { - // Handle XTC file - Xtc xtc(APP_STATE.openEpubPath, "/.crosspoint"); - if (xtc.load()) { - if (!xtc.getTitle().empty()) { - lastBookTitle = std::string(xtc.getTitle()); - } - if (!xtc.getAuthor().empty()) { - lastBookAuthor = std::string(xtc.getAuthor()); - } - // Try to generate thumbnail image for Continue Reading card - if (xtc.generateThumbBmp()) { - coverBmpPath = xtc.getThumbBmpPath(); + + if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".epub")) { + Epub epub(APP_STATE.openEpubPath, "/.crosspoint"); + epub.load(false); + if (!epub.getTitle().empty()) lastBookTitle = epub.getTitle(); + if (!epub.getAuthor().empty()) lastBookAuthor = epub.getAuthor(); + if (epub.generateThumbBmp()) { + coverBmpPath = epub.getThumbBmpPath(); hasCoverImage = true; } - } - // Remove extension from title if we don't have metadata - if (StringUtils::checkFileExtension(lastBookTitle, ".xtch")) { - lastBookTitle.resize(lastBookTitle.length() - 5); - } else if (StringUtils::checkFileExtension(lastBookTitle, ".xtc")) { - lastBookTitle.resize(lastBookTitle.length() - 4); + // Get progress info from the same loaded epub + FsFile f; + if (SdMan.openFileForRead("HOME", epub.getCachePath() + "/progress.bin", f)) { + uint8_t data[4]; + if (f.read(data, 4) == 4) { + int spineIndex = data[0] + (data[1] << 8); + int spineCount = epub.getSpineItemsCount(); + cachedCurrentPage = std::to_string(spineIndex + 1); + cachedTotalPages = std::to_string(spineCount); + if (spineCount > 0) cachedProgressPercent = (spineIndex * 100) / spineCount; + auto spineEntry = epub.getSpineItem(spineIndex); + if (spineEntry.tocIndex != -1) { + cachedChapterTitle = epub.getTocItem(spineEntry.tocIndex).title; + } + } + f.close(); + } + } else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtc") || + StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtch")) { + Xtc xtc(APP_STATE.openEpubPath, "/.crosspoint"); + if (xtc.load()) { + if (!xtc.getTitle().empty()) lastBookTitle = xtc.getTitle(); + if (xtc.generateThumbBmp()) { + coverBmpPath = xtc.getThumbBmpPath(); + hasCoverImage = true; + } + // Get progress from same loaded xtc + FsFile f; + if (SdMan.openFileForRead("HOME", xtc.getCachePath() + "/progress.bin", f)) { + uint8_t data[4]; + if (f.read(data, 4) == 4) { + uint32_t currentPage = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); + uint32_t totalPages = xtc.getPageCount(); + cachedCurrentPage = std::to_string(currentPage + 1); + cachedTotalPages = std::to_string(totalPages); + if (totalPages > 0) cachedProgressPercent = (currentPage * 100) / totalPages; + cachedChapterTitle = "Page " + cachedCurrentPage; + } + f.close(); + } + } + // Remove extension from title if we don't have metadata + if (StringUtils::checkFileExtension(lastBookTitle, ".xtch")) { + lastBookTitle.resize(lastBookTitle.length() - 5); + } else if (StringUtils::checkFileExtension(lastBookTitle, ".xtc")) { + lastBookTitle.resize(lastBookTitle.length() - 4); + } } } } @@ -101,9 +143,6 @@ void HomeActivity::onEnter() { coverRendered = false; coverBufferStored = false; - // Load and cache recent books data (slow operation, do once) - loadRecentBooksData(); - // Trigger first update updateRequired = true; @@ -311,7 +350,7 @@ void HomeActivity::render() { lastBatteryCheck = now; } - // Always clear screen - ThemeEngine handles caching internally + // Always clear screen - required because parent containers draw backgrounds renderer.clearScreen(); ThemeEngine::ThemeContext context; @@ -352,73 +391,12 @@ void HomeActivity::render() { context.setBool("HasCover", hasContinueReading && hasCoverImage && !coverBmpPath.empty()); context.setBool("ShowInfoBox", true); - // Default values - std::string chapterTitle = ""; - std::string currentPageStr = "-"; - std::string totalPagesStr = "-"; - int progressPercent = 0; - - if (hasContinueReading) { - if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".epub")) { - Epub epub(APP_STATE.openEpubPath, "/.crosspoint"); - epub.load(false); - - // Read progress - FsFile f; - if (SdMan.openFileForRead("HOME", epub.getCachePath() + "/progress.bin", f)) { - uint8_t data[4]; - if (f.read(data, 4) == 4) { - int spineIndex = data[0] + (data[1] << 8); - int spineCount = epub.getSpineItemsCount(); - - currentPageStr = std::to_string(spineIndex + 1); // Display 1-based - totalPagesStr = std::to_string(spineCount); - - if (spineCount > 0) { - progressPercent = (spineIndex * 100) / spineCount; - } - - // Resolve Chapter Title - auto spineEntry = epub.getSpineItem(spineIndex); - if (spineEntry.tocIndex != -1) { - auto tocEntry = epub.getTocItem(spineEntry.tocIndex); - chapterTitle = tocEntry.title; - } - } - f.close(); - } - } else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtc") || - StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".xtch")) { - Xtc xtc(APP_STATE.openEpubPath, "/.crosspoint"); - if (xtc.load()) { - // Read progress - FsFile f; - if (SdMan.openFileForRead("HOME", xtc.getCachePath() + "/progress.bin", f)) { - uint8_t data[4]; - if (f.read(data, 4) == 4) { - uint32_t currentPage = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24); - uint32_t totalPages = xtc.getPageCount(); - - currentPageStr = std::to_string(currentPage + 1); // 1-based - totalPagesStr = std::to_string(totalPages); - - if (totalPages > 0) { - progressPercent = (currentPage * 100) / totalPages; - } - - chapterTitle = "Page " + currentPageStr; - } - f.close(); - } - } - } - } - - context.setString("BookChapter", chapterTitle); - context.setString("BookCurrentPage", currentPageStr); - context.setString("BookTotalPages", totalPagesStr); - context.setInt("BookProgressPercent", progressPercent); - context.setString("BookProgressPercentStr", std::to_string(progressPercent)); + // Use cached values (loaded in onEnter, NOT every render) + context.setString("BookChapter", cachedChapterTitle); + context.setString("BookCurrentPage", cachedCurrentPage); + context.setString("BookTotalPages", cachedTotalPages); + context.setInt("BookProgressPercent", cachedProgressPercent); + context.setString("BookProgressPercentStr", std::to_string(cachedProgressPercent)); // --- Main Menu Data --- // Menu items start after the book slot diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index 9802d1a3..052f651e 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -28,12 +28,20 @@ class HomeActivity final : public Activity { bool coverRendered = false; // Track if cover has been rendered once bool coverBufferStored = false; // Track if cover buffer is stored uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image + bool needsFullRender = true; // Force full render (first time or after layout changes) + int lastSelectorIndex = -1; // Track selection for incremental updates std::string lastBookTitle; std::string lastBookAuthor; std::string coverBmpPath; uint8_t cachedBatteryLevel = 0; uint32_t lastBatteryCheck = 0; + // Cached "continue reading" info (loaded once in onEnter, NOT every render!) + std::string cachedChapterTitle; + std::string cachedCurrentPage; + std::string cachedTotalPages; + int cachedProgressPercent = 0; + // Cached recent books data (loaded once in onEnter) std::vector cachedRecentBooks; const std::function onContinueReading;