Files
Crosspoint/src/activities/home/HomeActivity.cpp
Justin Mitchell bfb8aacdae Add framebuffer release and font metadata access
Introduce methods to temporarily release the 48KB framebuffer during memory-intensive chapter builds, allowing the memory to be reused. Add font metadata query methods (hasGlyphMeta, ensureAdvance) that check glyph existence and fetch advance metrics without loading full bitmap data, avoiding expensive SD reads during layout measurement.
2026-07-08 04:46:25 -04:00

282 lines
9.4 KiB
C++

#include "HomeActivity.h"
#include <Bitmap.h>
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
#include <Utf8.h>
#include <Xtc.h>
#include <cstring>
#include <vector>
#include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "MappedInputManager.h"
#include "OpdsServerStore.h"
#include "RecentBooksStore.h"
#include "components/UITheme.h"
#include "fontIds.h"
#include "util/BookCoverUtils.h"
int HomeActivity::getMenuItemCount() const {
int count = 4; // File Browser, Recents, File transfer, Settings
if (!recentBooks.empty()) {
count += recentBooks.size();
}
if (hasOpdsServers) {
count++;
}
return count;
}
void HomeActivity::loadRecentBooks(int maxBooks) {
recentBooks.clear();
const auto& books = RECENT_BOOKS.getBooks();
recentBooks.reserve(std::min(static_cast<int>(books.size()), maxBooks));
for (const RecentBook& book : books) {
// Limit to maximum number of recent books
if (recentBooks.size() >= maxBooks) {
break;
}
// Skip if file no longer exists
if (RecentBooksStore::isMissing(book)) {
continue;
}
recentBooks.push_back(book);
}
}
void HomeActivity::loadRecentCovers(int coverHeight) {
recentsLoading = true;
bool showingLoading = false;
Rect popupRect;
int progress = 0;
for (RecentBook& book : recentBooks) {
if (!book.coverBmpPath.empty()) {
std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight);
// Missing OR a stale zero-byte sentinel (transient-failure leftover)
// both warrant an attempt; a real thumb or a decided-coverless marker
// (1 byte) short-circuits without opening the container.
if (BookCoverUtils::thumbAttemptNeeded(coverPath)) {
// If epub, generate the Continue Reading thumbnail from its cover
if (FsHelpers::hasEpubExtension(book.path)) {
if (!showingLoading) {
showingLoading = true;
popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP));
}
GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size()));
bool success = BookCoverUtils::generateThumbBmp(book.path, coverHeight);
if (!success) {
RECENT_BOOKS.updateBook(book.path, book.title, book.author, "");
book.coverBmpPath = "";
}
coverRendered = false;
requestUpdate();
} else if (FsHelpers::hasXtcExtension(book.path)) {
// Handle XTC file
Xtc xtc(book.path, "/.crosspoint");
if (xtc.load()) {
// Try to generate thumbnail image for Continue Reading card
if (!showingLoading) {
showingLoading = true;
popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP));
}
GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size()));
bool success = xtc.generateThumbBmp(coverHeight);
if (!success) {
RECENT_BOOKS.updateBook(book.path, book.title, book.author, "");
book.coverBmpPath = "";
}
coverRendered = false;
requestUpdate();
}
}
}
}
progress++;
}
recentsLoaded = true;
recentsLoading = false;
}
void HomeActivity::onEnter() {
Activity::onEnter();
hasOpdsServers = OPDS_STORE.hasServers();
const auto& metrics = UITheme::getInstance().getMetrics();
loadRecentBooks(metrics.homeRecentBooksCount);
const auto base = static_cast<int>(recentBooks.size());
selectorIndex = initialMenuItem == HomeMenuItem::NONE ? 0 : base + menuItemToIndex(initialMenuItem, hasOpdsServers);
// Trigger first update
requestUpdate();
}
void HomeActivity::onExit() {
Activity::onExit();
// Free the stored cover buffer if any
freeCoverBuffer();
}
bool HomeActivity::storeCoverBuffer() {
// render() must have already set the cover rect; without it we'd be back to
// cloning the whole framebuffer.
if (coverRectW <= 0 || coverRectH <= 0) return false;
freeCoverBuffer();
const size_t needed = renderer.getRegionByteSize(coverRectX, coverRectY, coverRectW, coverRectH);
if (needed == 0) return false;
coverBuffer = static_cast<uint8_t*>(malloc(needed));
if (!coverBuffer) {
LOG_ERR("HOME", "OOM: cover buffer (%u bytes)", (unsigned)needed);
return false;
}
coverBufferSize = needed;
if (!renderer.copyRegionToBuffer(coverRectX, coverRectY, coverRectW, coverRectH, coverBuffer, coverBufferSize)) {
free(coverBuffer);
coverBuffer = nullptr;
coverBufferSize = 0;
return false;
}
return true;
}
bool HomeActivity::restoreCoverBuffer() {
if (!coverBuffer || coverRectW <= 0 || coverRectH <= 0) return false;
return renderer.copyBufferToRegion(coverRectX, coverRectY, coverRectW, coverRectH, coverBuffer, coverBufferSize);
}
void HomeActivity::freeCoverBuffer() {
if (coverBuffer) {
free(coverBuffer);
coverBuffer = nullptr;
}
coverBufferSize = 0;
coverBufferStored = false;
}
void HomeActivity::loop() {
const int menuCount = getMenuItemCount();
buttonNavigator.onNext([this, menuCount] {
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, menuCount);
requestUpdate();
});
buttonNavigator.onPrevious([this, menuCount] {
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, menuCount);
requestUpdate();
});
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (selectorIndex < recentBooks.size()) {
onSelectBook(recentBooks[selectorIndex].path);
} else {
const int menuIndex = selectorIndex - static_cast<int>(recentBooks.size());
switch (indexToMenuItem(menuIndex, hasOpdsServers)) {
case HomeMenuItem::FILE_BROWSER:
onFileBrowserOpen();
break;
case HomeMenuItem::RECENTS:
onRecentsOpen();
break;
case HomeMenuItem::OPDS_BROWSER:
onOpdsBrowserOpen();
break;
case HomeMenuItem::FILE_TRANSFER:
onFileTransferOpen();
break;
case HomeMenuItem::SETTINGS_MENU:
onSettingsOpen();
break;
default:
break;
}
}
}
}
void HomeActivity::render(RenderLock&&) {
const auto& metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen();
bool bufferRestored = coverBufferStored && restoreCoverBuffer();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.homeTopPadding},
metrics.homeContinueReadingInMenu && !recentBooks.empty() ? recentBooks[0].title.c_str() : nullptr);
// Record the tile rect so storeCoverBuffer (called from the theme) knows
// which sub-region of the framebuffer to snapshot. ~16 KB in Portrait
// instead of the 48 KB full framebuffer the previous bind captured.
coverRectX = 0;
coverRectY = metrics.homeTopPadding;
coverRectW = pageWidth;
coverRectH = metrics.homeCoverTileHeight;
GUI.drawRecentBookCover(renderer, Rect{0, metrics.homeTopPadding, pageWidth, metrics.homeCoverTileHeight},
recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored,
std::bind(&HomeActivity::storeCoverBuffer, this));
// Build menu items dynamically
std::vector<const char*> menuItems = {tr(STR_BROWSE_FILES), tr(STR_MENU_RECENT_BOOKS), tr(STR_FILE_TRANSFER),
tr(STR_SETTINGS_TITLE)};
std::vector<UIIcon> menuIcons = {Folder, Recent, Transfer, Settings};
if (hasOpdsServers) {
menuItems.insert(menuItems.begin() + 2, tr(STR_OPDS_BROWSER));
menuIcons.insert(menuIcons.begin() + 2, Library);
}
if (metrics.homeContinueReadingInMenu && !recentBooks.empty()) {
// Insert Continue Reading at the top if enabled in theme
menuItems.insert(menuItems.begin(), tr(STR_CONTINUE_READING));
menuIcons.insert(menuIcons.begin(), Book);
}
GUI.drawButtonMenu(
renderer,
Rect{0, metrics.homeTopPadding + metrics.homeCoverTileHeight + metrics.homeMenuTopOffset, pageWidth,
pageHeight - (metrics.headerHeight + metrics.homeTopPadding + metrics.verticalSpacing +
metrics.homeMenuTopOffset + metrics.buttonHintsHeight)},
static_cast<int>(menuItems.size()),
metrics.homeContinueReadingInMenu ? selectorIndex : selectorIndex - recentBooks.size(),
[&menuItems](int index) { return std::string(menuItems[index]); },
[&menuIcons](int index) { return menuIcons[index]; });
const auto labels = mappedInput.mapLabels("", tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
if (!firstRenderDone) {
firstRenderDone = true;
requestUpdate();
} else if (!recentsLoaded && !recentsLoading) {
recentsLoading = true;
loadRecentCovers(metrics.homeCoverHeight);
}
}
void HomeActivity::onSelectBook(const std::string& path) { activityManager.goToReader(path); }
void HomeActivity::onFileBrowserOpen() { activityManager.goToFileBrowser(); }
void HomeActivity::onRecentsOpen() { activityManager.goToRecentBooks(); }
void HomeActivity::onSettingsOpen() { activityManager.goToSettings(); }
void HomeActivity::onFileTransferOpen() { activityManager.goToFileTransfer(); }
void HomeActivity::onOpdsBrowserOpen() { activityManager.goToBrowser(); }