#include "EpubReaderChapterSelectionActivity.h" #include #include #include #include #include "MappedInputManager.h" #include "components/UIScale.h" #include "components/UITheme.h" namespace fui = freeink::ui; EpubReaderChapterSelectionActivity::EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::shared_ptr& epub, const int currentSpineIndex) : UiListActivity("EpubReaderChapterSelection", renderer, mappedInput), epub(epub), currentSpineIndex(currentSpineIndex) {} void EpubReaderChapterSelectionActivity::onEnter() { UiListActivity::onEnter(); // The reader underneath pins its page-render glyph arenas while this // overlay is up. clearCache() is heap-adaptive: below the retention floor // it frees them (the next page render's PrewarmScope rebuilds them at // normal page-turn cost), giving this list room to keep every row's // fallback glyphs resident — otherwise each repaint re-reads the visible // rows' glyphs from SD. if (auto* fcm = renderer.getFontCacheManager()) { fcm->clearCache(); } if (!epub) { return; } // Start with the current chapter at the top of the viewport; the first // screen build pulls the viewport to it (ListNav follow-on-build) and // materializes the row window there (refreshTocWindow in buildScreen). int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); if (tocIndex == -1) { tocIndex = 0; } nav.selected = tocIndex; } // Materialize the ListItem/label window starting at `start` (clamped). TOC // entries are SD LUT reads (getTocItem), so this runs only when the viewport // leaves the current window. Finishes with a batch prewarm of the window's // CJK fallback glyphs -- one bounded SD pass per list page; repaints inside // the window stay RAM-only. void EpubReaderChapterSelectionActivity::refreshTocWindow(const int start) { const int total = listCount(); int clamped = start; if (clamped > total - TOC_WINDOW) clamped = total - TOC_WINDOW; if (clamped < 0) clamped = 0; if (clamped == windowStart) return; windowCount = total - clamped < TOC_WINDOW ? total - clamped : TOC_WINDOW; for (int i = 0; i < windowCount; i++) { const auto tocItem = epub->getTocItem(clamped + i); std::string indent(tocItem.level > 0 ? (tocItem.level - 1) * 2 : 0, ' '); windowLabels[i] = indent + tocItem.title; fui::ListItem item; item.label = windowLabels[i].c_str(); item.actionValue = static_cast(clamped + i); windowItems[i] = item; } windowStart = clamped; struct PrewarmCtx { const std::string* labels; int count; } prewarmCtx{windowLabels, windowCount}; renderer.prewarmFallbackText( uiScaleSpec().bodyFontId, [](const void* ctx, uint32_t i) -> const char* { const auto* c = static_cast(ctx); return i < static_cast(c->count) ? c->labels[i].c_str() : nullptr; }, &prewarmCtx, static_cast(windowCount)); } void EpubReaderChapterSelectionActivity::activateIndex(const int index) { if (index < 0 || index >= listCount()) { return; } // The activated row leaves this screen (finish); a lingering flash would gray // an unrelated element on the next render. app.clearTapFlash(); nav.selected = index; const auto tocItem = epub->getTocItem(index); if (tocItem.spineIndex == -1) { ActivityResult result; result.isCancelled = true; setResult(std::move(result)); finish(); } else { setResult(ChapterResult{tocItem.spineIndex, tocItem.anchor}); finish(); } } bool EpubReaderChapterSelectionActivity::handleButtons() { if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { ActivityResult result; result.isCancelled = true; setResult(std::move(result)); finish(); return true; } if (!epub) { return true; } if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { activateIndex(nav.selected); return true; } return false; } void EpubReaderChapterSelectionActivity::buildScreen(UiScreen& screen) { const auto& metrics = UITheme::getInstance().getMetrics(); const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); // Content: the safe area minus the header band drawChrome paints the title in. screen.setContentMarginFromScreen(fui::Insets{ static_cast(safe.y + metrics.topPadding + metrics.headerHeight), static_cast(renderer.getScreenWidth() - (safe.x + safe.width)), static_cast(renderer.getScreenHeight() - (safe.y + safe.height)), static_cast(safe.x)}); screen.spacer(static_cast(metrics.verticalSpacing)); if (!epub) { return; } if (listCount() == 0) { screen.centeredText(tr(STR_NO_CHAPTERS), screen.theme().bodyText); return; } fui::ListProps props; props.count = static_cast(listCount()); props.action = ACTION_ROW; props.inputMask = fui::InputTouch; // physical buttons stay in loop() syncListViewport(screen, props); // Materialize the row window for the final viewport (syncListViewport just // applied follow/clamping to nav.top) and hand list() the window with its // absolute base index. refreshTocWindow(nav.top); props.items = windowItems; props.itemsWindowFirst = static_cast(windowStart); screen.list(props); } void EpubReaderChapterSelectionActivity::drawChrome() { const auto& metrics = UITheme::getInstance().getMetrics(); const Rect safe = UITheme::getInstance().getScreenSafeArea(renderer, true, false); GUI.drawHeader(renderer, Rect{safe.x, safe.y + metrics.topPadding, safe.width, metrics.headerHeight}, tr(STR_SELECT_CHAPTER)); }