From fd53c14a311fde2846f0337d47c6fa7373d632cc Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Tue, 3 Feb 2026 18:25:26 +0300 Subject: [PATCH] feat: enhance screen orientation handling across Epub reader activities --- src/activities/reader/EpubReaderActivity.cpp | 10 ++++ .../EpubReaderChapterSelectionActivity.cpp | 58 ++++++++++++------- .../reader/EpubReaderMenuActivity.cpp | 10 ++++ .../XtcReaderChapterSelectionActivity.cpp | 54 +++++++++++------ 4 files changed, 92 insertions(+), 40 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 26c4e97f..6c064aa6 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -20,6 +20,8 @@ constexpr unsigned long goHomeMs = 1000; constexpr int statusBarMargin = 19; constexpr int progressBarMarginTop = 1; +// 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: @@ -54,6 +56,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); renderingMutex = xSemaphoreCreateMutex(); @@ -225,6 +228,8 @@ void EpubReaderActivity::loop() { void EpubReaderActivity::onReaderMenuBack(const uint8_t orientation) { exitActivity(); + // Apply the user-selected orientation when the menu is dismissed. + // This ensures the menu can be navigated without immediately rotating the screen. applyOrientation(orientation); updateRequired = true; } @@ -308,10 +313,12 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction } void EpubReaderActivity::applyOrientation(const uint8_t orientation) { + // No-op if the selected orientation matches current settings. if (SETTINGS.orientation == orientation) { return; } + // Preserve current reading position so we can restore after reflow. xSemaphoreTake(renderingMutex, portMAX_DELAY); if (section) { cachedSpineIndex = currentSpineIndex; @@ -319,11 +326,14 @@ void EpubReaderActivity::applyOrientation(const uint8_t orientation) { nextPageNumber = section->currentPage; } + // Persist the selection so the reader keeps the new orientation on next launch. SETTINGS.orientation = orientation; SETTINGS.saveToFile(); + // Update renderer orientation to match the new logical coordinate system. applyReaderOrientation(renderer, SETTINGS.orientation); + // Reset section to force re-layout in the new orientation. section.reset(); xSemaphoreGive(renderingMutex); } diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index a77b37d0..c47db235 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -1,5 +1,6 @@ #include "EpubReaderChapterSelectionActivity.h" +#include #include #include "KOReaderCredentialStore.h" @@ -34,20 +35,18 @@ int EpubReaderChapterSelectionActivity::tocIndexFromItemIndex(int itemIndex) con int EpubReaderChapterSelectionActivity::getPageItems() const { // Layout constants used in renderScreen - constexpr int startY = 60; constexpr int lineHeight = 30; const int screenHeight = renderer.getScreenHeight(); - const int endY = screenHeight - lineHeight; - - const int availableHeight = endY - 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; + const auto orientation = renderer.getOrientation(); + // In inverted portrait, the button hints are drawn near the logical top. + // Reserve vertical space so list items do not collide with the hints. + const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; + const int hintGutterHeight = isPortraitInverted ? 50 : 0; + const int startY = 60 + hintGutterHeight; + const int availableHeight = screenHeight - startY - lineHeight; + // Clamp to at least one item to avoid division by zero and empty paging. + return std::max(1, availableHeight / lineHeight); } void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) { @@ -178,39 +177,54 @@ void EpubReaderChapterSelectionActivity::renderScreen() { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); + const auto orientation = renderer.getOrientation(); + // Landscape orientation: reserve a horizontal gutter for button hints. + const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise; + const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise; + // Inverted portrait: reserve vertical space for hints at the top. + const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; + const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0; + // Landscape CW places hints on the left edge; CCW keeps them on the right. + const int contentX = isLandscapeCw ? hintGutterWidth : 0; + const int contentWidth = pageWidth - hintGutterWidth; + const int hintGutterHeight = isPortraitInverted ? 50 : 0; + const int contentY = hintGutterHeight; const int pageItems = getPageItems(); const int totalItems = getTotalItems(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Go to Chapter", true, EpdFontFamily::BOLD); + // Manual centering to honor content gutters. + const int titleX = contentX + + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, "Go to Chapter", EpdFontFamily::BOLD)) / 2; + renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, "Go to Chapter", true, EpdFontFamily::BOLD); const auto pageStartIndex = selectorIndex / pageItems * pageItems; - renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30); + // Highlight only the content area, not the hint gutters. + renderer.fillRect(contentX, 60 + contentY + (selectorIndex % pageItems) * 30 - 2, contentWidth - 1, 30); for (int i = 0; i < pageItems; i++) { int itemIndex = pageStartIndex + i; if (itemIndex >= totalItems) break; - const int displayY = 60 + i * 30; + const int displayY = 60 + contentY + i * 30; const bool isSelected = (itemIndex == selectorIndex); if (isSyncItem(itemIndex)) { - renderer.drawText(UI_10_FONT_ID, 20, displayY, ">> Sync Progress", !isSelected); + // Sync option uses a fixed label and stays aligned to the content margin. + renderer.drawText(UI_10_FONT_ID, contentX + 20, displayY, ">> Sync Progress", !isSelected); } else { const int tocIndex = tocIndexFromItemIndex(itemIndex); auto item = epub->getTocItem(tocIndex); - const int indentSize = 20 + (item.level - 1) * 15; + // Indent per TOC level while keeping content within the gutter-safe region. + const int indentSize = contentX + 20 + (item.level - 1) * 15; const std::string chapterName = - renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - indentSize); + renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), contentWidth - 40 - indentSize); renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected); } } - // Skip button hints in landscape CW mode (they overlap content) - if (renderer.getOrientation() != GfxRenderer::LandscapeClockwise) { - const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); - renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - } + const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); } diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 2456bc30..8d80bbc0 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -58,6 +58,7 @@ void EpubReaderMenuActivity::loop() { } else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { const auto selectedAction = menuItems[selectedIndex].action; if (selectedAction == MenuAction::ROTATE_SCREEN) { + // Cycle orientation preview locally; actual rotation happens on menu exit. pendingOrientation = (pendingOrientation + 1) % orientationLabels.size(); updateRequired = true; return; @@ -72,6 +73,7 @@ void EpubReaderMenuActivity::loop() { // 3. CRITICAL: Return immediately. 'this' is likely deleted now. return; } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + // Return the pending orientation to the parent so it can apply on exit. onBack(pendingOrientation); return; // Also return here just in case } @@ -81,10 +83,15 @@ void EpubReaderMenuActivity::renderScreen() { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); const auto orientation = renderer.getOrientation(); + // Landscape orientation: button hints are drawn along a vertical edge, so we + // reserve a horizontal gutter to prevent overlap with menu content. const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise; const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise; + // Inverted portrait: button hints appear near the logical top, so we reserve + // vertical space to keep the header and list clear. const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0; + // Landscape CW places hints on the left edge; CCW keeps them on the right. const int contentX = isLandscapeCw ? hintGutterWidth : 0; const int contentWidth = pageWidth - hintGutterWidth; const int hintGutterHeight = isPortraitInverted ? 50 : 0; @@ -93,6 +100,7 @@ void EpubReaderMenuActivity::renderScreen() { // Title const std::string truncTitle = renderer.truncatedText(UI_12_FONT_ID, title.c_str(), contentWidth - 40, EpdFontFamily::BOLD); + // Manual centering so we can respect the content gutter. const int titleX = contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, truncTitle.c_str(), EpdFontFamily::BOLD)) / 2; renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, truncTitle.c_str(), true, EpdFontFamily::BOLD); @@ -106,12 +114,14 @@ void EpubReaderMenuActivity::renderScreen() { const bool isSelected = (static_cast(i) == selectedIndex); if (isSelected) { + // Highlight only the content area so we don't paint over hint gutters. renderer.fillRect(contentX, displayY, contentWidth - 1, lineHeight, true); } renderer.drawText(UI_10_FONT_ID, contentX + 20, displayY, menuItems[i].label.c_str(), !isSelected); if (menuItems[i].action == MenuAction::ROTATE_SCREEN) { + // Render current orientation value on the right edge of the content area. const auto value = orientationLabels[pendingOrientation]; const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected); diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index ad806a30..a67e0acc 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -1,5 +1,6 @@ #include "XtcReaderChapterSelectionActivity.h" +#include #include #include "MappedInputManager.h" @@ -10,18 +11,18 @@ constexpr int SKIP_PAGE_MS = 700; } // namespace int XtcReaderChapterSelectionActivity::getPageItems() const { - constexpr int startY = 60; constexpr int lineHeight = 30; const int screenHeight = renderer.getScreenHeight(); - const int endY = screenHeight - lineHeight; - - const int availableHeight = endY - startY; - int items = availableHeight / lineHeight; - if (items < 1) { - items = 1; - } - return items; + const auto orientation = renderer.getOrientation(); + // In inverted portrait, the hint row is drawn near the logical top. + // Reserve vertical space so the list starts below the hints. + const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; + const int hintGutterHeight = isPortraitInverted ? 50 : 0; + const int startY = 60 + hintGutterHeight; + const int availableHeight = screenHeight - startY - lineHeight; + // Clamp to at least one item to prevent empty page math. + return std::max(1, availableHeight / lineHeight); } int XtcReaderChapterSelectionActivity::findChapterIndexForPage(uint32_t page) const { @@ -131,29 +132,46 @@ void XtcReaderChapterSelectionActivity::renderScreen() { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); + const auto orientation = renderer.getOrientation(); + // Landscape orientation: reserve a horizontal gutter for button hints. + const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise; + const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise; + // Inverted portrait: reserve vertical space for hints at the top. + const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; + const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0; + // Landscape CW places hints on the left edge; CCW keeps them on the right. + const int contentX = isLandscapeCw ? hintGutterWidth : 0; + const int contentWidth = pageWidth - hintGutterWidth; + const int hintGutterHeight = isPortraitInverted ? 50 : 0; + const int contentY = hintGutterHeight; const int pageItems = getPageItems(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Chapter", true, EpdFontFamily::BOLD); + // Manual centering to honor content gutters. + const int titleX = contentX + + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, "Select Chapter", EpdFontFamily::BOLD)) / 2; + renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, "Select Chapter", true, EpdFontFamily::BOLD); const auto& chapters = xtc->getChapters(); if (chapters.empty()) { - renderer.drawCenteredText(UI_10_FONT_ID, 120, "No chapters"); + // Center the empty state within the gutter-safe content region. + const int emptyX = + contentX + (contentWidth - renderer.getTextWidth(UI_10_FONT_ID, "No chapters")) / 2; + renderer.drawText(UI_10_FONT_ID, emptyX, 120 + contentY, "No chapters"); renderer.displayBuffer(); return; } const auto pageStartIndex = selectorIndex / pageItems * pageItems; - renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30); + // Highlight only the content area, not the hint gutters. + renderer.fillRect(contentX, 60 + contentY + (selectorIndex % pageItems) * 30 - 2, contentWidth - 1, 30); for (int i = pageStartIndex; i < static_cast(chapters.size()) && i < pageStartIndex + pageItems; i++) { const auto& chapter = chapters[i]; const char* title = chapter.name.empty() ? "Unnamed" : chapter.name.c_str(); - renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % pageItems) * 30, title, i != selectorIndex); + renderer.drawText(UI_10_FONT_ID, contentX + 20, 60 + contentY + (i % pageItems) * 30, title, + i != selectorIndex); } - // Skip button hints in landscape CW mode (they overlap content) - if (renderer.getOrientation() != GfxRenderer::LandscapeClockwise) { - const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); - renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - } + const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); }