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
This commit is contained in:
@@ -25,6 +25,7 @@
|
|||||||
#include "LookedUpWordsActivity.h"
|
#include "LookedUpWordsActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "QrDisplayActivity.h"
|
#include "QrDisplayActivity.h"
|
||||||
|
#include "ReaderUtils.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
@@ -36,7 +37,6 @@
|
|||||||
namespace {
|
namespace {
|
||||||
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
|
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
|
||||||
constexpr unsigned long skipChapterMs = 700;
|
constexpr unsigned long skipChapterMs = 700;
|
||||||
constexpr unsigned long goHomeMs = 1000;
|
|
||||||
constexpr unsigned long longPressConfirmMs = 700;
|
constexpr unsigned long longPressConfirmMs = 700;
|
||||||
// pages per minute, first item is 1 to prevent division by zero if accessed
|
// pages per minute, first item is 1 to prevent division by zero if accessed
|
||||||
const std::vector<int> PAGE_TURN_LABELS = {1, 1, 3, 6, 12};
|
const std::vector<int> PAGE_TURN_LABELS = {1, 1, 3, 6, 12};
|
||||||
@@ -55,27 +55,6 @@ int clampPercent(int percent) {
|
|||||||
return 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
|
} // namespace
|
||||||
|
|
||||||
void EpubReaderActivity::onEnter() {
|
void EpubReaderActivity::onEnter() {
|
||||||
@@ -87,7 +66,7 @@ void EpubReaderActivity::onEnter() {
|
|||||||
|
|
||||||
// Configure screen orientation based on settings
|
// Configure screen orientation based on settings
|
||||||
// NOTE: This affects layout math and must be applied before any render calls.
|
// NOTE: This affects layout math and must be applied before any render calls.
|
||||||
applyReaderOrientation(renderer, SETTINGS.orientation);
|
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
|
||||||
|
|
||||||
epub->setupCacheDir();
|
epub->setupCacheDir();
|
||||||
|
|
||||||
@@ -314,13 +293,14 @@ void EpubReaderActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Long press BACK (1s+) goes to file selection
|
// 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() : "");
|
activityManager.goToFileBrowser(epub ? epub->getPath() : "");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short press BACK goes directly to home (or restores position if viewing footnote)
|
// 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) {
|
if (footnoteDepth > 0) {
|
||||||
restoreSavedPosition();
|
restoreSavedPosition();
|
||||||
return;
|
return;
|
||||||
@@ -329,20 +309,7 @@ void EpubReaderActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When long-press chapter skip is disabled, turn pages on press instead of release.
|
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||||
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));
|
|
||||||
|
|
||||||
if (!prevTriggered && !nextTriggered) {
|
if (!prevTriggered && !nextTriggered) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -771,7 +738,7 @@ void EpubReaderActivity::applyOrientation(const uint8_t orientation) {
|
|||||||
SETTINGS.saveToFile();
|
SETTINGS.saveToFile();
|
||||||
|
|
||||||
// Update renderer orientation to match the new logical coordinate system.
|
// 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.
|
// Reset section to force re-layout in the new orientation.
|
||||||
section.reset();
|
section.reset();
|
||||||
@@ -1121,12 +1088,8 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
}
|
}
|
||||||
// Double FAST_REFRESH handles ghosting for image pages; don't count toward full refresh cadence
|
// 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 {
|
} else {
|
||||||
renderer.displayBuffer();
|
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
|
||||||
pagesUntilFullRefresh--;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save bw buffer to reset buffer state after grayscale data sync
|
// Save bw buffer to reset buffer state after grayscale data sync
|
||||||
|
|||||||
89
src/activities/reader/ReaderUtils.h
Normal file
89
src/activities/reader/ReaderUtils.h
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <CrossPointSettings.h>
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
#include <Logging.h>
|
||||||
|
|
||||||
|
#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 <typename RenderFn>
|
||||||
|
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
|
||||||
@@ -9,14 +9,13 @@
|
|||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
|
#include "ReaderUtils.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr unsigned long goHomeMs = 1000;
|
|
||||||
constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading
|
constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading
|
||||||
|
|
||||||
// Cache file magic and version
|
// Cache file magic and version
|
||||||
constexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI"
|
constexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI"
|
||||||
constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes
|
constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes
|
||||||
@@ -29,23 +28,7 @@ void TxtReaderActivity::onEnter() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure screen orientation based on settings
|
ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
txt->setupCacheDir();
|
txt->setupCacheDir();
|
||||||
|
|
||||||
@@ -75,31 +58,19 @@ void TxtReaderActivity::onExit() {
|
|||||||
|
|
||||||
void TxtReaderActivity::loop() {
|
void TxtReaderActivity::loop() {
|
||||||
// Long press BACK (1s+) goes to file selection
|
// 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() : "");
|
activityManager.goToFileBrowser(txt ? txt->getPath() : "");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Short press BACK goes directly to home
|
// 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();
|
onGoHome();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// When long-press chapter skip is disabled, turn pages on press instead of release.
|
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput);
|
||||||
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));
|
|
||||||
|
|
||||||
if (!prevTriggered && !nextTriggered) {
|
if (!prevTriggered && !nextTriggered) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -398,34 +369,10 @@ void TxtReaderActivity::renderPage() {
|
|||||||
renderLines();
|
renderLines();
|
||||||
renderStatusBar();
|
renderStatusBar();
|
||||||
|
|
||||||
if (pagesUntilFullRefresh <= 1) {
|
ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh);
|
||||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
|
||||||
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
|
|
||||||
} else {
|
|
||||||
renderer.displayBuffer();
|
|
||||||
pagesUntilFullRefresh--;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grayscale rendering pass (for anti-aliased fonts)
|
|
||||||
if (SETTINGS.textAntiAliasing) {
|
if (SETTINGS.textAntiAliasing) {
|
||||||
// Save BW buffer for restoration after grayscale pass
|
ReaderUtils::renderAntiAliased(renderer, [&renderLines]() { renderLines(); });
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user