From dd280bdc9727cdefc8ef59b24807164233143a26 Mon Sep 17 00:00:00 2001 From: Tannay Date: Sun, 28 Dec 2025 05:33:20 -0500 Subject: [PATCH] Rotation Support (#77) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • What is the goal of this PR? Implement a horizontal EPUB reading mode so books can be read in landscape orientation (both 90° and 270°), while keeping the rest of the UI in portrait. • What changes are included? ◦ Rendering / Display ▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal, LandscapeFlipped) and made: ▪ drawPixel, drawImage, displayWindow map logical coordinates differently depending on orientation. ▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical dimensions (480×800 in portrait, 800×480 in landscape). ◦ Settings / Configuration ▪ Extended CrossPointSettings with: ▪ landscapeReading (toggle for portrait vs. landscape EPUB reading). ▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal holding directions are supported). ▪ Updated settings serialization/deserialization to persist these fields while remaining backward‑compatible with existing settings files. ▪ Updated SettingsActivity to expose two new toggles: ▪ “Landscape Reading” ▪ “Flip Landscape (swap top/bottom)” ◦ EPUB Reader ▪ In EpubReaderActivity: ▪ On onEnter, set GfxRenderer orientation based on the new settings (Portrait, LandscapeNormal, or LandscapeFlipped). ▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings, etc. continue to render as before. ▪ Adjusted renderStatusBar to position the status bar and battery indicator relative to GfxRenderer::getScreenHeight() instead of hard‑coded Y coordinates, so it stays correctly at the bottom in both portrait and landscape. ◦ EPUB Caching / Layout ▪ Extended Section cache metadata (section.bin) to include the logical screenWidth and screenHeight used when pages were generated; bumped SECTION_FILE_VERSION. ▪ Updated loadCacheMetadata to compare: ▪ font/margins/line compression/extraParagraphSpacing and screen dimensions; mismatches now invalidate and clear the cache. ▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so portrait and landscape caches are kept separate and correctly sized. Additional Context • Cache behavior / migration ◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected as incompatible and their caches cleared and rebuilt once per chapter when first opened after this change. ◦ Within a given orientation, caches will be reused as before. Switching orientation (portrait ↔ landscape) will cause a one‑time re‑index of each chapter in the new orientation. • Scope and risks ◦ Orientation changes are scoped to the EPUB reader; the Home screen, Settings, WiFi selection, sleep screens, and web server UI continue to assume portrait orientation. ◦ The renderer’s orientation is a static/global setting; if future code uses GfxRenderer outside the reader while a reader instance is active, it should be aware that orientation is no longer implicitly fixed. ◦ All drawing primitives now go through orientation‑aware coordinate transforms; any code that previously relied on edge‑case behavior or out‑of‑bounds writes might surface as logged “Outside range” warnings instead. • Testing suggestions / areas to focus on ◦ Verify in hardware: ▪ Portrait mode still renders correctly (boot, home, settings, WiFi, reader). ▪ Landscape reading in both directions: ▪ Landscape Reading = ON, Flip Landscape = OFF. ▪ Landscape Reading = ON, Flip Landscape = ON. ▪ Status bar (page X/Y, % progress, battery icon) is fully visible and aligned at the bottom in all three combinations. ◦ Open the same book: ▪ In portrait first, then switch to landscape and reopen it. ▪ Confirm that: ▪ Old portrait caches are rebuilt once for landscape (you should see the “Indexing…” page). ▪ Progress save/restore still works (resume opens to the correct page in the current orientation). ◦ Ensure grayscale rendering (the secondary pass in EpubReaderActivity::renderContents) still looks correct in both orientations. --------- Co-authored-by: Dave Allie --- lib/Epub/Epub/Page.cpp | 8 +- lib/Epub/Epub/Page.h | 6 +- lib/Epub/Epub/ParsedText.cpp | 4 +- lib/Epub/Epub/ParsedText.h | 2 +- lib/Epub/Epub/Section.cpp | 46 +++---- lib/Epub/Epub/Section.h | 13 +- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 13 +- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h | 16 +-- lib/GfxRenderer/GfxRenderer.cpp | 119 ++++++++++++++---- lib/GfxRenderer/GfxRenderer.h | 28 ++++- src/CrossPointSettings.cpp | 8 +- src/CrossPointSettings.h | 10 ++ src/activities/boot_sleep/BootActivity.cpp | 6 +- src/activities/boot_sleep/SleepActivity.cpp | 2 +- src/activities/reader/EpubReaderActivity.cpp | 94 +++++++++----- src/activities/reader/EpubReaderActivity.h | 5 +- .../EpubReaderChapterSelectionActivity.cpp | 34 +++-- .../EpubReaderChapterSelectionActivity.h | 4 + .../reader/FileSelectionActivity.cpp | 2 +- src/activities/settings/SettingsActivity.cpp | 10 +- .../util/FullScreenMessageActivity.cpp | 2 +- src/activities/util/KeyboardEntryActivity.cpp | 4 +- 22 files changed, 297 insertions(+), 139 deletions(-) diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index b41dd3c4..15e50d08 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -7,7 +7,9 @@ namespace { constexpr uint8_t PAGE_FILE_VERSION = 3; } -void PageLine::render(GfxRenderer& renderer, const int fontId) { block->render(renderer, fontId, xPos, yPos); } +void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) { + block->render(renderer, fontId, xPos + xOffset, yPos + yOffset); +} void PageLine::serialize(File& file) { serialization::writePod(file, xPos); @@ -27,9 +29,9 @@ std::unique_ptr PageLine::deserialize(File& file) { return std::unique_ptr(new PageLine(std::move(tb), xPos, yPos)); } -void Page::render(GfxRenderer& renderer, const int fontId) const { +void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const { for (auto& element : elements) { - element->render(renderer, fontId); + element->render(renderer, fontId, xOffset, yOffset); } } diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 10266534..f43e4987 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -17,7 +17,7 @@ class PageElement { int16_t yPos; explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {} virtual ~PageElement() = default; - virtual void render(GfxRenderer& renderer, int fontId) = 0; + virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0; virtual void serialize(File& file) = 0; }; @@ -28,7 +28,7 @@ class PageLine final : public PageElement { public: PageLine(std::shared_ptr block, const int16_t xPos, const int16_t yPos) : PageElement(xPos, yPos), block(std::move(block)) {} - void render(GfxRenderer& renderer, int fontId) override; + void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override; void serialize(File& file) override; static std::unique_ptr deserialize(File& file); }; @@ -37,7 +37,7 @@ class Page { public: // the list of block index and line numbers on this page std::vector> elements; - void render(GfxRenderer& renderer, int fontId) const; + void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; void serialize(File& file) const; static std::unique_ptr deserialize(File& file); }; diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index 7a045d56..0e850f31 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -18,14 +18,14 @@ void ParsedText::addWord(std::string word, const EpdFontStyle fontStyle) { } // Consumes data to minimize memory usage -void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const int horizontalMargin, +void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fontId, const int viewportWidth, const std::function)>& processLine, const bool includeLastLine) { if (words.empty()) { return; } - const int pageWidth = renderer.getScreenWidth() - horizontalMargin; + const int pageWidth = viewportWidth; const int spaceWidth = renderer.getSpaceWidth(fontId); const auto wordWidths = calculateWordWidths(renderer, fontId); const auto lineBreakIndices = computeLineBreaks(pageWidth, spaceWidth, wordWidths); diff --git a/lib/Epub/Epub/ParsedText.h b/lib/Epub/Epub/ParsedText.h index 7fdb1286..2696407f 100644 --- a/lib/Epub/Epub/ParsedText.h +++ b/lib/Epub/Epub/ParsedText.h @@ -34,7 +34,7 @@ class ParsedText { TextBlock::BLOCK_STYLE getStyle() const { return style; } size_t size() const { return words.size(); } bool isEmpty() const { return words.empty(); } - void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, int horizontalMargin, + void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, int viewportWidth, const std::function)>& processLine, bool includeLastLine = true); }; diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index bd46d35c..7b815792 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -8,8 +8,8 @@ #include "parsers/ChapterHtmlSlimParser.h" namespace { -constexpr uint8_t SECTION_FILE_VERSION = 5; -} +constexpr uint8_t SECTION_FILE_VERSION = 6; +} // namespace void Section::onPageComplete(std::unique_ptr page) { const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin"; @@ -26,9 +26,8 @@ void Section::onPageComplete(std::unique_ptr page) { pageCount++; } -void Section::writeCacheMetadata(const int fontId, const float lineCompression, const int marginTop, - const int marginRight, const int marginBottom, const int marginLeft, - const bool extraParagraphSpacing) const { +void Section::writeCacheMetadata(const int fontId, const float lineCompression, const bool extraParagraphSpacing, + const int viewportWidth, const int viewportHeight) const { File outputFile; if (!FsHelpers::openFileForWrite("SCT", cachePath + "/section.bin", outputFile)) { return; @@ -36,18 +35,15 @@ void Section::writeCacheMetadata(const int fontId, const float lineCompression, serialization::writePod(outputFile, SECTION_FILE_VERSION); serialization::writePod(outputFile, fontId); serialization::writePod(outputFile, lineCompression); - serialization::writePod(outputFile, marginTop); - serialization::writePod(outputFile, marginRight); - serialization::writePod(outputFile, marginBottom); - serialization::writePod(outputFile, marginLeft); serialization::writePod(outputFile, extraParagraphSpacing); + serialization::writePod(outputFile, viewportWidth); + serialization::writePod(outputFile, viewportHeight); serialization::writePod(outputFile, pageCount); outputFile.close(); } -bool Section::loadCacheMetadata(const int fontId, const float lineCompression, const int marginTop, - const int marginRight, const int marginBottom, const int marginLeft, - const bool extraParagraphSpacing) { +bool Section::loadCacheMetadata(const int fontId, const float lineCompression, const bool extraParagraphSpacing, + const int viewportWidth, const int viewportHeight) { const auto sectionFilePath = cachePath + "/section.bin"; File inputFile; if (!FsHelpers::openFileForRead("SCT", sectionFilePath, inputFile)) { @@ -65,20 +61,18 @@ bool Section::loadCacheMetadata(const int fontId, const float lineCompression, c return false; } - int fileFontId, fileMarginTop, fileMarginRight, fileMarginBottom, fileMarginLeft; + int fileFontId, fileViewportWidth, fileViewportHeight; float fileLineCompression; bool fileExtraParagraphSpacing; serialization::readPod(inputFile, fileFontId); serialization::readPod(inputFile, fileLineCompression); - serialization::readPod(inputFile, fileMarginTop); - serialization::readPod(inputFile, fileMarginRight); - serialization::readPod(inputFile, fileMarginBottom); - serialization::readPod(inputFile, fileMarginLeft); serialization::readPod(inputFile, fileExtraParagraphSpacing); + serialization::readPod(inputFile, fileViewportWidth); + serialization::readPod(inputFile, fileViewportHeight); - if (fontId != fileFontId || lineCompression != fileLineCompression || marginTop != fileMarginTop || - marginRight != fileMarginRight || marginBottom != fileMarginBottom || marginLeft != fileMarginLeft || - extraParagraphSpacing != fileExtraParagraphSpacing) { + if (fontId != fileFontId || lineCompression != fileLineCompression || + extraParagraphSpacing != fileExtraParagraphSpacing || viewportWidth != fileViewportWidth || + viewportHeight != fileViewportHeight) { inputFile.close(); Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis()); clearCache(); @@ -113,9 +107,9 @@ bool Section::clearCache() const { return true; } -bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop, - const int marginRight, const int marginBottom, const int marginLeft, - const bool extraParagraphSpacing, const std::function& progressSetupFn, +bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const bool extraParagraphSpacing, + const int viewportWidth, const int viewportHeight, + const std::function& progressSetupFn, const std::function& progressFn) { constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB const auto localPath = epub->getSpineItem(spineIndex).href; @@ -163,8 +157,8 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression, } ChapterHtmlSlimParser visitor( - tmpHtmlPath, renderer, fontId, lineCompression, marginTop, marginRight, marginBottom, marginLeft, - extraParagraphSpacing, [this](std::unique_ptr page) { this->onPageComplete(std::move(page)); }, progressFn); + tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, viewportWidth, viewportHeight, + [this](std::unique_ptr page) { this->onPageComplete(std::move(page)); }, progressFn); success = visitor.parseAndBuildPages(); SD.remove(tmpHtmlPath.c_str()); @@ -173,7 +167,7 @@ bool Section::persistPageDataToSD(const int fontId, const float lineCompression, return false; } - writeCacheMetadata(fontId, lineCompression, marginTop, marginRight, marginBottom, marginLeft, extraParagraphSpacing); + writeCacheMetadata(fontId, lineCompression, extraParagraphSpacing, viewportWidth, viewportHeight); return true; } diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index 09a2f90b..a1a62163 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -13,8 +13,8 @@ class Section { GfxRenderer& renderer; std::string cachePath; - void writeCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, - int marginLeft, bool extraParagraphSpacing) const; + void writeCacheMetadata(int fontId, float lineCompression, bool extraParagraphSpacing, int viewportWidth, + int viewportHeight) const; void onPageComplete(std::unique_ptr page); public: @@ -27,13 +27,12 @@ class Section { renderer(renderer), cachePath(epub->getCachePath() + "/" + std::to_string(spineIndex)) {} ~Section() = default; - bool loadCacheMetadata(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, - int marginLeft, bool extraParagraphSpacing); + bool loadCacheMetadata(int fontId, float lineCompression, bool extraParagraphSpacing, int viewportWidth, + int viewportHeight); void setupCacheDir() const; bool clearCache() const; - bool persistPageDataToSD(int fontId, float lineCompression, int marginTop, int marginRight, int marginBottom, - int marginLeft, bool extraParagraphSpacing, - const std::function& progressSetupFn = nullptr, + bool persistPageDataToSD(int fontId, float lineCompression, bool extraParagraphSpacing, int viewportWidth, + int viewportHeight, const std::function& progressSetupFn = nullptr, const std::function& progressFn = nullptr); std::unique_ptr loadPageFromSD() const; }; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index d2f1c3e6..b2dc2c01 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -155,7 +155,7 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char if (self->currentTextBlock->size() > 750) { Serial.printf("[%lu] [EHP] Text block too long, splitting into multiple pages\n", millis()); self->currentTextBlock->layoutAndExtractLines( - self->renderer, self->fontId, self->marginLeft + self->marginRight, + self->renderer, self->fontId, self->viewportWidth, [self](const std::shared_ptr& textBlock) { self->addLineToPage(textBlock); }, false); } } @@ -301,15 +301,14 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr line) { const int lineHeight = renderer.getLineHeight(fontId) * lineCompression; - const int pageHeight = GfxRenderer::getScreenHeight() - marginTop - marginBottom; - if (currentPageNextY + lineHeight > pageHeight) { + if (currentPageNextY + lineHeight > viewportHeight) { completePageFn(std::move(currentPage)); currentPage.reset(new Page()); - currentPageNextY = marginTop; + currentPageNextY = 0; } - currentPage->elements.push_back(std::make_shared(line, marginLeft, currentPageNextY)); + currentPage->elements.push_back(std::make_shared(line, 0, currentPageNextY)); currentPageNextY += lineHeight; } @@ -321,12 +320,12 @@ void ChapterHtmlSlimParser::makePages() { if (!currentPage) { currentPage.reset(new Page()); - currentPageNextY = marginTop; + currentPageNextY = 0; } const int lineHeight = renderer.getLineHeight(fontId) * lineCompression; currentTextBlock->layoutAndExtractLines( - renderer, fontId, marginLeft + marginRight, + renderer, fontId, viewportWidth, [this](const std::shared_ptr& textBlock) { addLineToPage(textBlock); }); // Extra paragraph spacing if enabled if (extraParagraphSpacing) { diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 7d753173..53bbbb4f 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -32,11 +32,9 @@ class ChapterHtmlSlimParser { int16_t currentPageNextY = 0; int fontId; float lineCompression; - int marginTop; - int marginRight; - int marginBottom; - int marginLeft; bool extraParagraphSpacing; + int viewportWidth; + int viewportHeight; void startNewTextBlock(TextBlock::BLOCK_STYLE style); void makePages(); @@ -47,19 +45,17 @@ class ChapterHtmlSlimParser { public: explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId, - const float lineCompression, const int marginTop, const int marginRight, - const int marginBottom, const int marginLeft, const bool extraParagraphSpacing, + const float lineCompression, const bool extraParagraphSpacing, const int viewportWidth, + const int viewportHeight, const std::function)>& completePageFn, const std::function& progressFn = nullptr) : filepath(filepath), renderer(renderer), fontId(fontId), lineCompression(lineCompression), - marginTop(marginTop), - marginRight(marginRight), - marginBottom(marginBottom), - marginLeft(marginLeft), extraParagraphSpacing(extraParagraphSpacing), + viewportWidth(viewportWidth), + viewportHeight(viewportHeight), completePageFn(completePageFn), progressFn(progressFn) {} ~ChapterHtmlSlimParser() = default; diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index bcd88087..c9a2554d 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -4,6 +4,37 @@ void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); } +void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int* rotatedY) const { + switch (orientation) { + case Portrait: { + // Logical portrait (480x800) → panel (800x480) + // Rotation: 90 degrees clockwise + *rotatedX = y; + *rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x; + break; + } + case LandscapeClockwise: { + // Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right) + *rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - x; + *rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - y; + break; + } + case PortraitInverted: { + // Logical portrait (480x800) → panel (800x480) + // Rotation: 90 degrees counter-clockwise + *rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - y; + *rotatedY = x; + break; + } + case LandscapeCounterClockwise: { + // Logical landscape (800x480) aligned with panel orientation + *rotatedX = x; + *rotatedY = y; + break; + } + } +} + void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); @@ -13,15 +44,14 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { return; } - // Rotate coordinates: portrait (480x800) -> landscape (800x480) - // Rotation: 90 degrees clockwise - const int rotatedX = y; - const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x; + int rotatedX = 0; + int rotatedY = 0; + rotateCoordinates(x, y, &rotatedX, &rotatedY); - // Bounds checking (portrait: 480x800) + // Bounds checking against physical panel dimensions if (rotatedX < 0 || rotatedX >= EInkDisplay::DISPLAY_WIDTH || rotatedY < 0 || rotatedY >= EInkDisplay::DISPLAY_HEIGHT) { - Serial.printf("[%lu] [GFX] !! Outside range (%d, %d)\n", millis(), x, y); + Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, rotatedX, rotatedY); return; } @@ -115,8 +145,11 @@ void GfxRenderer::fillRect(const int x, const int y, const int width, const int } void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const { - // Flip X and Y for portrait mode - einkDisplay.drawImage(bitmap, y, x, height, width); + // TODO: Rotate bits + int rotatedX = 0; + int rotatedY = 0; + rotateCoordinates(x, y, &rotatedX, &rotatedY); + einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height); } void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, @@ -205,23 +238,34 @@ void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) cons einkDisplay.displayBuffer(refreshMode); } -void GfxRenderer::displayWindow(const int x, const int y, const int width, const int height) const { - // Rotate coordinates from portrait (480x800) to landscape (800x480) - // Rotation: 90 degrees clockwise - // Portrait coordinates: (x, y) with dimensions (width, height) - // Landscape coordinates: (rotatedX, rotatedY) with dimensions (rotatedWidth, rotatedHeight) - - const int rotatedX = y; - const int rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x - width + 1; - const int rotatedWidth = height; - const int rotatedHeight = width; - - einkDisplay.displayWindow(rotatedX, rotatedY, rotatedWidth, rotatedHeight); +// Note: Internal driver treats screen in command orientation; this library exposes a logical orientation +int GfxRenderer::getScreenWidth() const { + switch (orientation) { + case Portrait: + case PortraitInverted: + // 480px wide in portrait logical coordinates + return EInkDisplay::DISPLAY_HEIGHT; + case LandscapeClockwise: + case LandscapeCounterClockwise: + // 800px wide in landscape logical coordinates + return EInkDisplay::DISPLAY_WIDTH; + } + return EInkDisplay::DISPLAY_HEIGHT; } -// Note: Internal driver treats screen in command orientation, this library treats in portrait orientation -int GfxRenderer::getScreenWidth() { return EInkDisplay::DISPLAY_HEIGHT; } -int GfxRenderer::getScreenHeight() { return EInkDisplay::DISPLAY_WIDTH; } +int GfxRenderer::getScreenHeight() const { + switch (orientation) { + case Portrait: + case PortraitInverted: + // 800px tall in portrait logical coordinates + return EInkDisplay::DISPLAY_WIDTH; + case LandscapeClockwise: + case LandscapeCounterClockwise: + // 480px tall in landscape logical coordinates + return EInkDisplay::DISPLAY_HEIGHT; + } + return EInkDisplay::DISPLAY_WIDTH; +} int GfxRenderer::getSpaceWidth(const int fontId) const { if (fontMap.count(fontId) == 0) { @@ -432,3 +476,32 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp, *x += glyph->advanceX; } + +void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const { + switch (orientation) { + case Portrait: + *outTop = VIEWABLE_MARGIN_TOP; + *outRight = VIEWABLE_MARGIN_RIGHT; + *outBottom = VIEWABLE_MARGIN_BOTTOM; + *outLeft = VIEWABLE_MARGIN_LEFT; + break; + case LandscapeClockwise: + *outTop = VIEWABLE_MARGIN_LEFT; + *outRight = VIEWABLE_MARGIN_TOP; + *outBottom = VIEWABLE_MARGIN_RIGHT; + *outLeft = VIEWABLE_MARGIN_BOTTOM; + break; + case PortraitInverted: + *outTop = VIEWABLE_MARGIN_BOTTOM; + *outRight = VIEWABLE_MARGIN_LEFT; + *outBottom = VIEWABLE_MARGIN_TOP; + *outLeft = VIEWABLE_MARGIN_RIGHT; + break; + case LandscapeCounterClockwise: + *outTop = VIEWABLE_MARGIN_RIGHT; + *outRight = VIEWABLE_MARGIN_BOTTOM; + *outBottom = VIEWABLE_MARGIN_LEFT; + *outLeft = VIEWABLE_MARGIN_TOP; + break; + } +} diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 00a525dd..d1083a0e 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -12,6 +12,14 @@ class GfxRenderer { public: enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB }; + // Logical screen orientation from the perspective of callers + enum Orientation { + Portrait, // 480x800 logical coordinates (current default) + LandscapeClockwise, // 800x480 logical coordinates, rotated 180° (swap top/bottom) + PortraitInverted, // 480x800 logical coordinates, inverted + LandscapeCounterClockwise // 800x480 logical coordinates, native panel orientation + }; + private: static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory static constexpr size_t BW_BUFFER_NUM_CHUNKS = EInkDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE; @@ -20,24 +28,35 @@ class GfxRenderer { EInkDisplay& einkDisplay; RenderMode renderMode; + Orientation orientation; uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr}; std::map fontMap; void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState, EpdFontStyle style) const; void freeBwBufferChunks(); + void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const; public: - explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW) {} + explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {} ~GfxRenderer() = default; + static constexpr int VIEWABLE_MARGIN_TOP = 9; + static constexpr int VIEWABLE_MARGIN_RIGHT = 3; + static constexpr int VIEWABLE_MARGIN_BOTTOM = 3; + static constexpr int VIEWABLE_MARGIN_LEFT = 3; + // Setup void insertFont(int fontId, EpdFontFamily font); + // Orientation control (affects logical width/height and coordinate transforms) + void setOrientation(const Orientation o) { orientation = o; } + Orientation getOrientation() const { return orientation; } + // Screen ops - static int getScreenWidth(); - static int getScreenHeight(); + int getScreenWidth() const; + int getScreenHeight() const; void displayBuffer(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const; - // EXPERIMENTAL: Windowed update - display only a rectangular region (portrait coordinates) + // EXPERIMENTAL: Windowed update - display only a rectangular region void displayWindow(int x, int y, int width, int height) const; void invertScreen() const; void clearScreen(uint8_t color = 0xFF) const; @@ -72,4 +91,5 @@ class GfxRenderer { uint8_t* getFrameBuffer() const; static size_t getBufferSize(); void grayscaleRevert() const; + void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const; }; diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 83ba59d1..93284222 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -10,7 +10,8 @@ CrossPointSettings CrossPointSettings::instance; namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; -constexpr uint8_t SETTINGS_COUNT = 4; +// Increment this when adding new persisted settings fields +constexpr uint8_t SETTINGS_COUNT = 5; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace @@ -29,6 +30,7 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, extraParagraphSpacing); serialization::writePod(outputFile, shortPwrBtn); serialization::writePod(outputFile, statusBar); + serialization::writePod(outputFile, orientation); outputFile.close(); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); @@ -52,7 +54,7 @@ bool CrossPointSettings::loadFromFile() { uint8_t fileSettingsCount = 0; serialization::readPod(inputFile, fileSettingsCount); - // load settings that exist + // load settings that exist (support older files with fewer fields) uint8_t settingsRead = 0; do { serialization::readPod(inputFile, sleepScreen); @@ -63,6 +65,8 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, statusBar); if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, orientation); + if (++settingsRead >= fileSettingsCount) break; } while (false); inputFile.close(); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index ab591bef..2b99664e 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -21,6 +21,13 @@ class CrossPointSettings { // Status bar display type enum enum STATUS_BAR_MODE { NONE = 0, NO_PROGRESS = 1, FULL = 2 }; + enum ORIENTATION { + PORTRAIT = 0, // 480x800 logical coordinates (current default) + LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom) + INVERTED = 2, // 480x800 logical coordinates, inverted + LANDSCAPE_CCW = 3 // 800x480 logical coordinates, native panel orientation + }; + // Sleep screen settings uint8_t sleepScreen = DARK; // Status bar settings @@ -29,6 +36,9 @@ class CrossPointSettings { uint8_t extraParagraphSpacing = 1; // Duration of the power button press uint8_t shortPwrBtn = 0; + // EPUB reading orientation settings + // 0 = portrait (default), 1 = landscape clockwise, 2 = inverted, 3 = landscape counter-clockwise + uint8_t orientation = PORTRAIT; ~CrossPointSettings() = default; diff --git a/src/activities/boot_sleep/BootActivity.cpp b/src/activities/boot_sleep/BootActivity.cpp index 78a12482..a1530882 100644 --- a/src/activities/boot_sleep/BootActivity.cpp +++ b/src/activities/boot_sleep/BootActivity.cpp @@ -8,11 +8,11 @@ void BootActivity::onEnter() { Activity::onEnter(); - const auto pageWidth = GfxRenderer::getScreenWidth(); - const auto pageHeight = GfxRenderer::getScreenHeight(); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); renderer.clearScreen(); - renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128); + renderer.drawImage(CrossLarge, (pageWidth + 128) / 2, (pageHeight - 128) / 2, 128, 128); renderer.drawCenteredText(UI_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "BOOTING"); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION); diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 4bc70f57..6ff348e5 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -112,7 +112,7 @@ void SleepActivity::renderDefaultSleepScreen() const { const auto pageHeight = renderer.getScreenHeight(); renderer.clearScreen(); - renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128); + renderer.drawImage(CrossLarge, (pageWidth + 128) / 2, (pageHeight - 128) / 2, 128, 128); renderer.drawCenteredText(UI_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, BOLD); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING"); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 0dfda4bb..f0eed254 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -16,10 +16,8 @@ constexpr int pagesPerRefresh = 15; constexpr unsigned long skipChapterMs = 700; constexpr unsigned long goHomeMs = 1000; constexpr float lineCompression = 0.95f; -constexpr int marginTop = 8; -constexpr int marginRight = 10; -constexpr int marginBottom = 22; -constexpr int marginLeft = 10; +constexpr int horizontalPadding = 5; +constexpr int statusBarMargin = 19; } // namespace void EpubReaderActivity::taskTrampoline(void* param) { @@ -34,6 +32,24 @@ void EpubReaderActivity::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; + } + renderingMutex = xSemaphoreCreateMutex(); epub->setupCacheDir(); @@ -67,6 +83,9 @@ void EpubReaderActivity::onEnter() { void EpubReaderActivity::onExit() { ActivityWithSubactivity::onExit(); + // Reset orientation back to portrait for the rest of the UI + renderer.setOrientation(GfxRenderer::Orientation::Portrait); + // Wait until not rendering to delete task to avoid killing mid-instruction to EPD xSemaphoreTake(renderingMutex, portMAX_DELAY); if (displayTaskHandle) { @@ -219,12 +238,24 @@ void EpubReaderActivity::renderScreen() { return; } + // Apply screen viewable areas and additional padding + int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; + renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, + &orientedMarginLeft); + orientedMarginLeft += horizontalPadding; + orientedMarginRight += horizontalPadding; + orientedMarginBottom += statusBarMargin; + if (!section) { const auto filepath = epub->getSpineItem(currentSpineIndex).href; Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex); section = std::unique_ptr
(new Section(epub, currentSpineIndex, renderer)); - if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, marginLeft, - SETTINGS.extraParagraphSpacing)) { + + const auto viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; + const auto viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom; + + if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, SETTINGS.extraParagraphSpacing, viewportWidth, + viewportHeight)) { Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); // Progress bar dimensions @@ -236,8 +267,8 @@ void EpubReaderActivity::renderScreen() { const int boxWidthNoBar = textWidth + boxMargin * 2; const int boxHeightWithBar = renderer.getLineHeight(READER_FONT_ID) + barHeight + boxMargin * 3; const int boxHeightNoBar = renderer.getLineHeight(READER_FONT_ID) + boxMargin * 2; - const int boxXWithBar = (GfxRenderer::getScreenWidth() - boxWidthWithBar) / 2; - const int boxXNoBar = (GfxRenderer::getScreenWidth() - boxWidthNoBar) / 2; + const int boxXWithBar = (renderer.getScreenWidth() - boxWidthWithBar) / 2; + const int boxXNoBar = (renderer.getScreenWidth() - boxWidthNoBar) / 2; constexpr int boxY = 50; const int barX = boxXWithBar + (boxWidthWithBar - barWidth) / 2; const int barY = boxY + renderer.getLineHeight(READER_FONT_ID) + boxMargin * 2; @@ -254,8 +285,7 @@ void EpubReaderActivity::renderScreen() { section->setupCacheDir(); // Setup callback - only called for chapters >= 50KB, redraws with progress bar - auto progressSetup = [this, boxXWithBar, boxWidthWithBar, boxHeightWithBar, boxMargin, barX, barY, barWidth, - barHeight]() { + auto progressSetup = [this, boxXWithBar, boxWidthWithBar, boxHeightWithBar, barX, barY] { renderer.fillRect(boxXWithBar, boxY, boxWidthWithBar, boxHeightWithBar, false); renderer.drawText(READER_FONT_ID, boxXWithBar + boxMargin, boxY + boxMargin, "Indexing..."); renderer.drawRect(boxXWithBar + 5, boxY + 5, boxWidthWithBar - 10, boxHeightWithBar - 10); @@ -270,8 +300,8 @@ void EpubReaderActivity::renderScreen() { renderer.displayBuffer(EInkDisplay::FAST_REFRESH); }; - if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, - marginLeft, SETTINGS.extraParagraphSpacing, progressSetup, progressCallback)) { + if (!section->persistPageDataToSD(READER_FONT_ID, lineCompression, SETTINGS.extraParagraphSpacing, viewportWidth, + viewportHeight, progressSetup, progressCallback)) { Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis()); section.reset(); return; @@ -292,7 +322,7 @@ void EpubReaderActivity::renderScreen() { if (section->pageCount == 0) { Serial.printf("[%lu] [ERS] No pages to render\n", millis()); renderer.drawCenteredText(READER_FONT_ID, 300, "Empty chapter", true, BOLD); - renderStatusBar(); + renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderer.displayBuffer(); return; } @@ -300,7 +330,7 @@ void EpubReaderActivity::renderScreen() { if (section->currentPage < 0 || section->currentPage >= section->pageCount) { Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage, section->pageCount); renderer.drawCenteredText(READER_FONT_ID, 300, "Out of bounds", true, BOLD); - renderStatusBar(); + renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderer.displayBuffer(); return; } @@ -314,7 +344,7 @@ void EpubReaderActivity::renderScreen() { return renderScreen(); } const auto start = millis(); - renderContents(std::move(p)); + renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft); Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start); } @@ -330,9 +360,11 @@ void EpubReaderActivity::renderScreen() { } } -void EpubReaderActivity::renderContents(std::unique_ptr page) { - page->render(renderer, READER_FONT_ID); - renderStatusBar(); +void EpubReaderActivity::renderContents(std::unique_ptr page, const int orientedMarginTop, + const int orientedMarginRight, const int orientedMarginBottom, + const int orientedMarginLeft) { + page->render(renderer, READER_FONT_ID, orientedMarginLeft, orientedMarginTop); + renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); if (pagesUntilFullRefresh <= 1) { renderer.displayBuffer(EInkDisplay::HALF_REFRESH); pagesUntilFullRefresh = pagesPerRefresh; @@ -349,13 +381,13 @@ void EpubReaderActivity::renderContents(std::unique_ptr page) { { renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); - page->render(renderer, READER_FONT_ID); + page->render(renderer, READER_FONT_ID, orientedMarginLeft, orientedMarginTop); renderer.copyGrayscaleLsbBuffers(); // Render and copy to MSB buffer renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); - page->render(renderer, READER_FONT_ID); + page->render(renderer, READER_FONT_ID, orientedMarginLeft, orientedMarginTop); renderer.copyGrayscaleMsbBuffers(); // display grayscale part @@ -367,7 +399,8 @@ void EpubReaderActivity::renderContents(std::unique_ptr page) { renderer.restoreBwBuffer(); } -void EpubReaderActivity::renderStatusBar() const { +void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const int orientedMarginBottom, + const int orientedMarginLeft) const { // determine visible status bar elements const bool showProgress = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; const bool showBattery = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || @@ -375,8 +408,9 @@ void EpubReaderActivity::renderStatusBar() const { const bool showChapterTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; - // height variable shared by all elements - constexpr auto textY = 776; + // Position status bar near the bottom of the logical screen, regardless of orientation + const auto screenHeight = renderer.getScreenHeight(); + const auto textY = screenHeight - orientedMarginBottom - 2; int percentageTextWidth = 0; int progressTextWidth = 0; @@ -389,7 +423,7 @@ void EpubReaderActivity::renderStatusBar() const { const std::string progress = std::to_string(section->currentPage + 1) + "/" + std::to_string(section->pageCount) + " " + std::to_string(bookProgress) + "%"; progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str()); - renderer.drawText(SMALL_FONT_ID, GfxRenderer::getScreenWidth() - marginRight - progressTextWidth, textY, + renderer.drawText(SMALL_FONT_ID, renderer.getScreenWidth() - orientedMarginRight - progressTextWidth, textY, progress.c_str()); } @@ -398,13 +432,13 @@ void EpubReaderActivity::renderStatusBar() const { const uint16_t percentage = battery.readPercentage(); const auto percentageText = std::to_string(percentage) + "%"; percentageTextWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()); - renderer.drawText(SMALL_FONT_ID, 20 + marginLeft, textY, percentageText.c_str()); + renderer.drawText(SMALL_FONT_ID, 20 + orientedMarginLeft, textY, percentageText.c_str()); // 1 column on left, 2 columns on right, 5 columns of battery body constexpr int batteryWidth = 15; constexpr int batteryHeight = 10; - constexpr int x = marginLeft; - constexpr int y = 783; + const int x = orientedMarginLeft; + const int y = screenHeight - orientedMarginBottom + 5; // Top line renderer.drawLine(x, y, x + batteryWidth - 4, y); @@ -429,9 +463,9 @@ void EpubReaderActivity::renderStatusBar() const { if (showChapterTitle) { // Centered chatper title text // Page width minus existing content with 30px padding on each side - const int titleMarginLeft = 20 + percentageTextWidth + 30 + marginLeft; - const int titleMarginRight = progressTextWidth + 30 + marginRight; - const int availableTextWidth = GfxRenderer::getScreenWidth() - titleMarginLeft - titleMarginRight; + const int titleMarginLeft = 20 + percentageTextWidth + 30 + orientedMarginLeft; + const int titleMarginRight = progressTextWidth + 30 + orientedMarginRight; + const int availableTextWidth = renderer.getScreenWidth() - titleMarginLeft - titleMarginRight; const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); std::string title; diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 143f56b1..f1abc92d 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -22,8 +22,9 @@ class EpubReaderActivity final : public ActivityWithSubactivity { static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); void renderScreen(); - void renderContents(std::unique_ptr p); - void renderStatusBar() const; + void renderContents(std::unique_ptr page, int orientedMarginTop, int orientedMarginRight, + int orientedMarginBottom, int orientedMarginLeft); + void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const; public: explicit EpubReaderActivity(GfxRenderer& renderer, InputManager& inputManager, std::unique_ptr epub, diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 4b7b7ec2..090415d1 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -7,10 +7,26 @@ #include "config.h" namespace { -constexpr int PAGE_ITEMS = 24; +// Time threshold for treating a long press as a page-up/page-down constexpr int SKIP_PAGE_MS = 700; } // namespace +int EpubReaderChapterSelectionActivity::getPageItems() const { + // Layout constants used in renderScreen + constexpr int startY = 60; + constexpr int lineHeight = 30; + + const int screenHeight = renderer.getScreenHeight(); + const int availableHeight = screenHeight - startY; + int items = availableHeight / lineHeight; + + // Ensure we always have at least one item per page to avoid division by zero + if (items < 1) { + items = 1; + } + return items; +} + void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) { auto* self = static_cast(param); self->displayTaskLoop(); @@ -56,6 +72,7 @@ void EpubReaderChapterSelectionActivity::loop() { inputManager.wasReleased(InputManager::BTN_DOWN) || inputManager.wasReleased(InputManager::BTN_RIGHT); const bool skipPage = inputManager.getHeldTime() > SKIP_PAGE_MS; + const int pageItems = getPageItems(); if (inputManager.wasReleased(InputManager::BTN_CONFIRM)) { onSelectSpineIndex(selectorIndex); @@ -64,14 +81,14 @@ void EpubReaderChapterSelectionActivity::loop() { } else if (prevReleased) { if (skipPage) { selectorIndex = - ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + epub->getSpineItemsCount()) % epub->getSpineItemsCount(); + ((selectorIndex / pageItems - 1) * pageItems + epub->getSpineItemsCount()) % epub->getSpineItemsCount(); } else { selectorIndex = (selectorIndex + epub->getSpineItemsCount() - 1) % epub->getSpineItemsCount(); } updateRequired = true; } else if (nextReleased) { if (skipPage) { - selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % epub->getSpineItemsCount(); + selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % epub->getSpineItemsCount(); } else { selectorIndex = (selectorIndex + 1) % epub->getSpineItemsCount(); } @@ -95,17 +112,18 @@ void EpubReaderChapterSelectionActivity::renderScreen() { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); + const int pageItems = getPageItems(); renderer.drawCenteredText(READER_FONT_ID, 10, "Select Chapter", true, BOLD); - const auto pageStartIndex = selectorIndex / PAGE_ITEMS * PAGE_ITEMS; - renderer.fillRect(0, 60 + (selectorIndex % PAGE_ITEMS) * 30 + 2, pageWidth - 1, 30); - for (int i = pageStartIndex; i < epub->getSpineItemsCount() && i < pageStartIndex + PAGE_ITEMS; i++) { + const auto pageStartIndex = selectorIndex / pageItems * pageItems; + renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 + 2, pageWidth - 1, 30); + for (int i = pageStartIndex; i < epub->getSpineItemsCount() && i < pageStartIndex + pageItems; i++) { const int tocIndex = epub->getTocIndexForSpineIndex(i); if (tocIndex == -1) { - renderer.drawText(UI_FONT_ID, 20, 60 + (i % PAGE_ITEMS) * 30, "Unnamed", i != selectorIndex); + renderer.drawText(UI_FONT_ID, 20, 60 + (i % pageItems) * 30, "Unnamed", i != selectorIndex); } else { auto item = epub->getTocItem(tocIndex); - renderer.drawText(UI_FONT_ID, 20 + (item.level - 1) * 15, 60 + (i % PAGE_ITEMS) * 30, item.title.c_str(), + renderer.drawText(UI_FONT_ID, 20 + (item.level - 1) * 15, 60 + (i % pageItems) * 30, item.title.c_str(), i != selectorIndex); } } diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.h b/src/activities/reader/EpubReaderChapterSelectionActivity.h index 8c1adef8..fefd225c 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.h +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.h @@ -18,6 +18,10 @@ class EpubReaderChapterSelectionActivity final : public Activity { const std::function onGoBack; const std::function onSelectSpineIndex; + // Number of items that fit on a page, derived from logical screen height. + // This adapts automatically when switching between portrait and landscape. + int getPageItems() const; + static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); void renderScreen(); diff --git a/src/activities/reader/FileSelectionActivity.cpp b/src/activities/reader/FileSelectionActivity.cpp index 9a1490c5..6104da4e 100644 --- a/src/activities/reader/FileSelectionActivity.cpp +++ b/src/activities/reader/FileSelectionActivity.cpp @@ -158,7 +158,7 @@ void FileSelectionActivity::displayTaskLoop() { void FileSelectionActivity::render() const { renderer.clearScreen(); - const auto pageWidth = GfxRenderer::getScreenWidth(); + const auto pageWidth = renderer.getScreenWidth(); renderer.drawCenteredText(READER_FONT_ID, 10, "Books", true, BOLD); // Help text diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index b71a877c..71fe331f 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -9,13 +9,17 @@ // Define the static settings list namespace { -constexpr int settingsCount = 5; +constexpr int settingsCount = 6; const SettingInfo settingsList[settingsCount] = { // Should match with SLEEP_SCREEN_MODE {"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}}, {"Status Bar", SettingType::ENUM, &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}}, {"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing, {}}, {"Short Power Button Click", SettingType::TOGGLE, &CrossPointSettings::shortPwrBtn, {}}, + {"Reading Orientation", + SettingType::ENUM, + &CrossPointSettings::orientation, + {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}}, {"Check for updates", SettingType::ACTION, nullptr, {}}, }; } // namespace @@ -139,8 +143,8 @@ void SettingsActivity::displayTaskLoop() { void SettingsActivity::render() const { renderer.clearScreen(); - const auto pageWidth = GfxRenderer::getScreenWidth(); - const auto pageHeight = GfxRenderer::getScreenHeight(); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); // Draw header renderer.drawCenteredText(READER_FONT_ID, 10, "Settings", true, BOLD); diff --git a/src/activities/util/FullScreenMessageActivity.cpp b/src/activities/util/FullScreenMessageActivity.cpp index cf84cc5c..54740b61 100644 --- a/src/activities/util/FullScreenMessageActivity.cpp +++ b/src/activities/util/FullScreenMessageActivity.cpp @@ -8,7 +8,7 @@ void FullScreenMessageActivity::onEnter() { Activity::onEnter(); const auto height = renderer.getLineHeight(UI_FONT_ID); - const auto top = (GfxRenderer::getScreenHeight() - height) / 2; + const auto top = (renderer.getScreenHeight() - height) / 2; renderer.clearScreen(); renderer.drawCenteredText(UI_FONT_ID, top, text.c_str(), true, style); diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index 73f3dde7..8a72f1bc 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -235,7 +235,7 @@ void KeyboardEntryActivity::loop() { } void KeyboardEntryActivity::render() const { - const auto pageWidth = GfxRenderer::getScreenWidth(); + const auto pageWidth = renderer.getScreenWidth(); renderer.clearScreen(); @@ -329,7 +329,7 @@ void KeyboardEntryActivity::render() const { } // Draw help text at absolute bottom of screen (consistent with other screens) - const auto pageHeight = GfxRenderer::getScreenHeight(); + const auto pageHeight = renderer.getScreenHeight(); renderer.drawText(SMALL_FONT_ID, 10, pageHeight - 30, "Navigate: D-pad | Select: OK | Cancel: BACK"); renderer.displayBuffer(); }