From 0d828ba9868e21b660bbd939da2058a8042cc0cd Mon Sep 17 00:00:00 2001 From: cottongin Date: Sun, 8 Mar 2026 04:37:13 -0400 Subject: [PATCH] port: extract shared reader utilities (upstream PR #1329) Adapted from upstream PR #1329 (not yet merged). Adds ReaderUtils.h with shared orientation, page-turn detection, refresh cycle, and anti-aliased rendering utilities. Refactors EpubReaderActivity and TxtReaderActivity to use shared implementations instead of duplicated inline code. If/when #1329 is merged upstream, this commit should be dropped during the next sync and the upstream version used instead. Made-with: Cursor --- src/activities/reader/EpubReaderActivity.cpp | 53 ++---------- src/activities/reader/ReaderUtils.h | 89 ++++++++++++++++++++ src/activities/reader/TxtReaderActivity.cpp | 69 ++------------- 3 files changed, 105 insertions(+), 106 deletions(-) create mode 100644 src/activities/reader/ReaderUtils.h diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index b68a9b1a..4df4fc08 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -25,6 +25,7 @@ #include "LookedUpWordsActivity.h" #include "MappedInputManager.h" #include "QrDisplayActivity.h" +#include "ReaderUtils.h" #include "RecentBooksStore.h" #include "components/UITheme.h" #include "fontIds.h" @@ -36,7 +37,6 @@ namespace { // pagesPerRefresh now comes from SETTINGS.getRefreshFrequency() constexpr unsigned long skipChapterMs = 700; -constexpr unsigned long goHomeMs = 1000; constexpr unsigned long longPressConfirmMs = 700; // pages per minute, first item is 1 to prevent division by zero if accessed const std::vector PAGE_TURN_LABELS = {1, 1, 3, 6, 12}; @@ -55,27 +55,6 @@ int clampPercent(int percent) { return percent; } -// Apply the logical reader orientation to the renderer. -// This centralizes orientation mapping so we don't duplicate switch logic elsewhere. -void applyReaderOrientation(GfxRenderer& renderer, const uint8_t orientation) { - switch (orientation) { - case CrossPointSettings::ORIENTATION::PORTRAIT: - renderer.setOrientation(GfxRenderer::Orientation::Portrait); - break; - case CrossPointSettings::ORIENTATION::LANDSCAPE_CW: - renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise); - break; - case CrossPointSettings::ORIENTATION::INVERTED: - renderer.setOrientation(GfxRenderer::Orientation::PortraitInverted); - break; - case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW: - renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise); - break; - default: - break; - } -} - } // namespace void EpubReaderActivity::onEnter() { @@ -87,7 +66,7 @@ void EpubReaderActivity::onEnter() { // Configure screen orientation based on settings // NOTE: This affects layout math and must be applied before any render calls. - applyReaderOrientation(renderer, SETTINGS.orientation); + ReaderUtils::applyOrientation(renderer, SETTINGS.orientation); epub->setupCacheDir(); @@ -314,13 +293,14 @@ void EpubReaderActivity::loop() { } // Long press BACK (1s+) goes to file selection - if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) { + if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { activityManager.goToFileBrowser(epub ? epub->getPath() : ""); return; } // Short press BACK goes directly to home (or restores position if viewing footnote) - if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) { + if (mappedInput.wasReleased(MappedInputManager::Button::Back) && + mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) { if (footnoteDepth > 0) { restoreSavedPosition(); return; @@ -329,20 +309,7 @@ void EpubReaderActivity::loop() { return; } - // When long-press chapter skip is disabled, turn pages on press instead of release. - const bool usePressForPageTurn = !SETTINGS.longPressChapterSkip; - const bool prevTriggered = usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) || - mappedInput.wasPressed(MappedInputManager::Button::Left)) - : (mappedInput.wasReleased(MappedInputManager::Button::PageBack) || - mappedInput.wasReleased(MappedInputManager::Button::Left)); - const bool powerPageTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && - mappedInput.wasReleased(MappedInputManager::Button::Power); - const bool nextTriggered = usePressForPageTurn - ? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || powerPageTurn || - mappedInput.wasPressed(MappedInputManager::Button::Right)) - : (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn || - mappedInput.wasReleased(MappedInputManager::Button::Right)); - + auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput); if (!prevTriggered && !nextTriggered) { return; } @@ -771,7 +738,7 @@ void EpubReaderActivity::applyOrientation(const uint8_t orientation) { SETTINGS.saveToFile(); // Update renderer orientation to match the new logical coordinate system. - applyReaderOrientation(renderer, SETTINGS.orientation); + ReaderUtils::applyOrientation(renderer, SETTINGS.orientation); // Reset section to force re-layout in the new orientation. section.reset(); @@ -1121,12 +1088,8 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or renderer.displayBuffer(HalDisplay::HALF_REFRESH); } // Double FAST_REFRESH handles ghosting for image pages; don't count toward full refresh cadence - } else if (pagesUntilFullRefresh <= 1) { - renderer.displayBuffer(HalDisplay::HALF_REFRESH); - pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); } else { - renderer.displayBuffer(); - pagesUntilFullRefresh--; + ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh); } // Save bw buffer to reset buffer state after grayscale data sync diff --git a/src/activities/reader/ReaderUtils.h b/src/activities/reader/ReaderUtils.h new file mode 100644 index 00000000..ab3a1381 --- /dev/null +++ b/src/activities/reader/ReaderUtils.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include + +#include "MappedInputManager.h" + +namespace ReaderUtils { + +constexpr unsigned long GO_HOME_MS = 1000; + +inline void applyOrientation(GfxRenderer& renderer, const uint8_t orientation) { + switch (orientation) { + case CrossPointSettings::ORIENTATION::PORTRAIT: + renderer.setOrientation(GfxRenderer::Orientation::Portrait); + break; + case CrossPointSettings::ORIENTATION::LANDSCAPE_CW: + renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise); + break; + case CrossPointSettings::ORIENTATION::INVERTED: + renderer.setOrientation(GfxRenderer::Orientation::PortraitInverted); + break; + case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW: + renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise); + break; + default: + break; + } +} + +struct PageTurnResult { + bool prev; + bool next; +}; + +inline PageTurnResult detectPageTurn(const MappedInputManager& input) { + const bool usePress = !SETTINGS.longPressChapterSkip; + const bool prev = usePress ? (input.wasPressed(MappedInputManager::Button::PageBack) || + input.wasPressed(MappedInputManager::Button::Left)) + : (input.wasReleased(MappedInputManager::Button::PageBack) || + input.wasReleased(MappedInputManager::Button::Left)); + const bool powerTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && + input.wasReleased(MappedInputManager::Button::Power); + const bool next = usePress ? (input.wasPressed(MappedInputManager::Button::PageForward) || powerTurn || + input.wasPressed(MappedInputManager::Button::Right)) + : (input.wasReleased(MappedInputManager::Button::PageForward) || powerTurn || + input.wasReleased(MappedInputManager::Button::Right)); + return {prev, next}; +} + +inline void displayWithRefreshCycle(const GfxRenderer& renderer, int& pagesUntilFullRefresh) { + if (pagesUntilFullRefresh <= 1) { + renderer.displayBuffer(HalDisplay::HALF_REFRESH); + pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); + } else { + renderer.displayBuffer(); + pagesUntilFullRefresh--; + } +} + +// Grayscale anti-aliasing pass. Renders content twice (LSB + MSB) to build +// the grayscale buffer. Only the content callback is re-rendered -- status bars +// and other overlays should be drawn before calling this. +// Kept as a template to avoid std::function overhead; instantiated once per reader type. +template +void renderAntiAliased(GfxRenderer& renderer, RenderFn&& renderFn) { + if (!renderer.storeBwBuffer()) { + LOG_ERR("READER", "Failed to store BW buffer for anti-aliasing"); + return; + } + + renderer.clearScreen(0x00); + renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); + renderFn(); + renderer.copyGrayscaleLsbBuffers(); + + renderer.clearScreen(0x00); + renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); + renderFn(); + renderer.copyGrayscaleMsbBuffers(); + + renderer.displayGrayBuffer(); + renderer.setRenderMode(GfxRenderer::BW); + + renderer.restoreBwBuffer(); +} + +} // namespace ReaderUtils diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 756b7749..381ea6a8 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -9,14 +9,13 @@ #include "CrossPointSettings.h" #include "CrossPointState.h" #include "MappedInputManager.h" +#include "ReaderUtils.h" #include "RecentBooksStore.h" #include "components/UITheme.h" #include "fontIds.h" namespace { -constexpr unsigned long goHomeMs = 1000; 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 @@ -29,23 +28,7 @@ void TxtReaderActivity::onEnter() { return; } - // Configure screen orientation based on settings - switch (SETTINGS.orientation) { - case CrossPointSettings::ORIENTATION::PORTRAIT: - renderer.setOrientation(GfxRenderer::Orientation::Portrait); - break; - case CrossPointSettings::ORIENTATION::LANDSCAPE_CW: - renderer.setOrientation(GfxRenderer::Orientation::LandscapeClockwise); - break; - case CrossPointSettings::ORIENTATION::INVERTED: - renderer.setOrientation(GfxRenderer::Orientation::PortraitInverted); - break; - case CrossPointSettings::ORIENTATION::LANDSCAPE_CCW: - renderer.setOrientation(GfxRenderer::Orientation::LandscapeCounterClockwise); - break; - default: - break; - } + ReaderUtils::applyOrientation(renderer, SETTINGS.orientation); txt->setupCacheDir(); @@ -75,31 +58,19 @@ void TxtReaderActivity::onExit() { void TxtReaderActivity::loop() { // Long press BACK (1s+) goes to file selection - if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) { + if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { activityManager.goToFileBrowser(txt ? txt->getPath() : ""); return; } // Short press BACK goes directly to home - if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < goHomeMs) { + if (mappedInput.wasReleased(MappedInputManager::Button::Back) && + mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) { onGoHome(); return; } - // When long-press chapter skip is disabled, turn pages on press instead of release. - const bool usePressForPageTurn = !SETTINGS.longPressChapterSkip; - const bool prevTriggered = usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) || - mappedInput.wasPressed(MappedInputManager::Button::Left)) - : (mappedInput.wasReleased(MappedInputManager::Button::PageBack) || - mappedInput.wasReleased(MappedInputManager::Button::Left)); - const bool powerPageTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && - mappedInput.wasReleased(MappedInputManager::Button::Power); - const bool nextTriggered = usePressForPageTurn - ? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || powerPageTurn || - mappedInput.wasPressed(MappedInputManager::Button::Right)) - : (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn || - mappedInput.wasReleased(MappedInputManager::Button::Right)); - + auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput); if (!prevTriggered && !nextTriggered) { return; } @@ -398,34 +369,10 @@ void TxtReaderActivity::renderPage() { renderLines(); renderStatusBar(); - if (pagesUntilFullRefresh <= 1) { - renderer.displayBuffer(HalDisplay::HALF_REFRESH); - pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); - } else { - renderer.displayBuffer(); - pagesUntilFullRefresh--; - } + ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh); - // Grayscale rendering pass (for anti-aliased fonts) if (SETTINGS.textAntiAliasing) { - // Save BW buffer for restoration after grayscale pass - renderer.storeBwBuffer(); - - renderer.clearScreen(0x00); - renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); - renderLines(); - renderer.copyGrayscaleLsbBuffers(); - - renderer.clearScreen(0x00); - renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); - renderLines(); - renderer.copyGrayscaleMsbBuffers(); - - renderer.displayGrayBuffer(); - renderer.setRenderMode(GfxRenderer::BW); - - // Restore BW buffer - renderer.restoreBwBuffer(); + ReaderUtils::renderAntiAliased(renderer, [&renderLines]() { renderLines(); }); } }