From 465cc029ffc788c9e110decc156a2c9cbca9264b Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Tue, 3 Feb 2026 17:09:42 +0300 Subject: [PATCH 1/5] feat: add screen rotation functionality to Epub reader menu --- src/activities/reader/EpubReaderActivity.cpp | 34 +++++++++++++++++++ .../reader/EpubReaderMenuActivity.h | 3 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 5ccfb4fe..8afa8205 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -268,6 +268,40 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction xSemaphoreGive(renderingMutex); break; } + case EpubReaderMenuActivity::MenuAction::ROTATE_SCREEN: { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + if (section) { + cachedSpineIndex = currentSpineIndex; + cachedChapterTotalPageCount = section->pageCount; + nextPageNumber = section->currentPage; + } + + SETTINGS.orientation = (SETTINGS.orientation + 1) % CrossPointSettings::ORIENTATION_COUNT; + SETTINGS.saveToFile(); + + 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; + } + + section.reset(); + exitActivity(); + updateRequired = true; + xSemaphoreGive(renderingMutex); + break; + } case EpubReaderMenuActivity::MenuAction::GO_HOME: { // 2. Trigger the reader's "Go Home" callback if (onGoHome) { diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index bd253f81..7f7566f6 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -13,7 +13,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { public: - enum class MenuAction { SELECT_CHAPTER, GO_HOME, DELETE_CACHE }; + enum class MenuAction { SELECT_CHAPTER, ROTATE_SCREEN, GO_HOME, DELETE_CACHE }; explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const std::function& onBack, const std::function& onAction) @@ -33,6 +33,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { }; const std::vector menuItems = {{MenuAction::SELECT_CHAPTER, "Go to Chapter"}, + {MenuAction::ROTATE_SCREEN, "Rotate Screen"}, {MenuAction::GO_HOME, "Go Home"}, {MenuAction::DELETE_CACHE, "Delete Book Cache"}}; From a85ec570184219d05edce5594679421788ec64d9 Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Tue, 3 Feb 2026 17:38:31 +0300 Subject: [PATCH 2/5] feat: implement screen rotation feature in Epub reader menu --- src/activities/reader/EpubReaderActivity.cpp | 99 +++++++++---------- src/activities/reader/EpubReaderActivity.h | 3 +- .../reader/EpubReaderMenuActivity.cpp | 16 ++- .../reader/EpubReaderMenuActivity.h | 11 ++- 4 files changed, 69 insertions(+), 60 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 8afa8205..26c4e97f 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -20,22 +20,8 @@ constexpr unsigned long goHomeMs = 1000; constexpr int statusBarMargin = 19; constexpr int progressBarMarginTop = 1; -} // namespace - -void EpubReaderActivity::taskTrampoline(void* param) { - auto* self = static_cast(param); - self->displayTaskLoop(); -} - -void EpubReaderActivity::onEnter() { - ActivityWithSubactivity::onEnter(); - - if (!epub) { - return; - } - - // Configure screen orientation based on settings - switch (SETTINGS.orientation) { +void applyReaderOrientation(GfxRenderer& renderer, const uint8_t orientation) { + switch (orientation) { case CrossPointSettings::ORIENTATION::PORTRAIT: renderer.setOrientation(GfxRenderer::Orientation::Portrait); break; @@ -51,6 +37,24 @@ void EpubReaderActivity::onEnter() { default: break; } +} + +} // namespace + +void EpubReaderActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void EpubReaderActivity::onEnter() { + ActivityWithSubactivity::onEnter(); + + if (!epub) { + return; + } + + // Configure screen orientation based on settings + applyReaderOrientation(renderer, SETTINGS.orientation); renderingMutex = xSemaphoreCreateMutex(); @@ -127,11 +131,10 @@ void EpubReaderActivity::loop() { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { // Don't start activity transition while rendering xSemaphoreTake(renderingMutex, portMAX_DELAY); - const int currentPage = section ? section->currentPage : 0; - const int totalPages = section ? section->pageCount : 0; exitActivity(); enterNewActivity(new EpubReaderMenuActivity( - this->renderer, this->mappedInput, epub->getTitle(), [this]() { onReaderMenuBack(); }, + this->renderer, this->mappedInput, epub->getTitle(), SETTINGS.orientation, + [this](const uint8_t orientation) { onReaderMenuBack(orientation); }, [this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); })); xSemaphoreGive(renderingMutex); } @@ -220,8 +223,9 @@ void EpubReaderActivity::loop() { } } -void EpubReaderActivity::onReaderMenuBack() { +void EpubReaderActivity::onReaderMenuBack(const uint8_t orientation) { exitActivity(); + applyOrientation(orientation); updateRequired = true; } @@ -268,40 +272,6 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction xSemaphoreGive(renderingMutex); break; } - case EpubReaderMenuActivity::MenuAction::ROTATE_SCREEN: { - xSemaphoreTake(renderingMutex, portMAX_DELAY); - if (section) { - cachedSpineIndex = currentSpineIndex; - cachedChapterTotalPageCount = section->pageCount; - nextPageNumber = section->currentPage; - } - - SETTINGS.orientation = (SETTINGS.orientation + 1) % CrossPointSettings::ORIENTATION_COUNT; - SETTINGS.saveToFile(); - - 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; - } - - section.reset(); - exitActivity(); - updateRequired = true; - xSemaphoreGive(renderingMutex); - break; - } case EpubReaderMenuActivity::MenuAction::GO_HOME: { // 2. Trigger the reader's "Go Home" callback if (onGoHome) { @@ -337,6 +307,27 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction } } +void EpubReaderActivity::applyOrientation(const uint8_t orientation) { + if (SETTINGS.orientation == orientation) { + return; + } + + xSemaphoreTake(renderingMutex, portMAX_DELAY); + if (section) { + cachedSpineIndex = currentSpineIndex; + cachedChapterTotalPageCount = section->pageCount; + nextPageNumber = section->currentPage; + } + + SETTINGS.orientation = orientation; + SETTINGS.saveToFile(); + + applyReaderOrientation(renderer, SETTINGS.orientation); + + section.reset(); + xSemaphoreGive(renderingMutex); +} + void EpubReaderActivity::displayTaskLoop() { while (true) { if (updateRequired) { diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index ca7c0dc9..24d58e9c 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -29,8 +29,9 @@ class EpubReaderActivity final : public ActivityWithSubactivity { int orientedMarginBottom, int orientedMarginLeft); void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const; void saveProgress(int spineIndex, int currentPage, int pageCount); - void onReaderMenuBack(); + void onReaderMenuBack(uint8_t orientation); void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action); + void applyOrientation(uint8_t orientation); public: explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr epub, diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 5ce4881d..f5de4732 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -56,9 +56,15 @@ void EpubReaderMenuActivity::loop() { selectedIndex = (selectedIndex + 1) % menuItems.size(); updateRequired = true; } else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + const auto selectedAction = menuItems[selectedIndex].action; + if (selectedAction == MenuAction::ROTATE_SCREEN) { + pendingOrientation = (pendingOrientation + 1) % orientationLabels.size(); + updateRequired = true; + return; + } + // 1. Capture the callback and action locally auto actionCallback = onAction; - auto selectedAction = menuItems[selectedIndex].action; // 2. Execute the callback actionCallback(selectedAction); @@ -66,7 +72,7 @@ void EpubReaderMenuActivity::loop() { // 3. CRITICAL: Return immediately. 'this' is likely deleted now. return; } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - onBack(); + onBack(pendingOrientation); return; // Also return here just in case } } @@ -93,6 +99,12 @@ void EpubReaderMenuActivity::renderScreen() { } renderer.drawText(UI_10_FONT_ID, 20, displayY, menuItems[i].label.c_str(), !isSelected); + + if (menuItems[i].action == MenuAction::ROTATE_SCREEN) { + const auto value = orientationLabels[pendingOrientation]; + const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); + renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, displayY, value, !isSelected); + } } // Footer / Hints diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 7f7566f6..9f25d75d 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -16,9 +16,12 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { enum class MenuAction { SELECT_CHAPTER, ROTATE_SCREEN, GO_HOME, DELETE_CACHE }; explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, - const std::function& onBack, const std::function& onAction) + const uint8_t currentOrientation, + const std::function& onBack, + const std::function& onAction) : ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput), title(title), + pendingOrientation(currentOrientation), onBack(onBack), onAction(onAction) {} @@ -33,7 +36,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { }; const std::vector menuItems = {{MenuAction::SELECT_CHAPTER, "Go to Chapter"}, - {MenuAction::ROTATE_SCREEN, "Rotate Screen"}, + {MenuAction::ROTATE_SCREEN, "Reading Orientation"}, {MenuAction::GO_HOME, "Go Home"}, {MenuAction::DELETE_CACHE, "Delete Book Cache"}}; @@ -42,8 +45,10 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; std::string title = "Reader Menu"; + uint8_t pendingOrientation = 0; + const std::vector orientationLabels = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}; - const std::function onBack; + const std::function onBack; const std::function onAction; static void taskTrampoline(void* param); From ecfb7e424e6eb456a0ca639537fb8a38a17f85b0 Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Tue, 3 Feb 2026 17:52:41 +0300 Subject: [PATCH 3/5] feat: orientation adjustments in Epub reader --- .../reader/EpubReaderMenuActivity.cpp | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index f5de4732..2456bc30 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -80,14 +80,25 @@ void EpubReaderMenuActivity::loop() { void EpubReaderMenuActivity::renderScreen() { renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth(); + const auto orientation = renderer.getOrientation(); + const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise; + const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise; + const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; + const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0; + const int contentX = isLandscapeCw ? hintGutterWidth : 0; + const int contentWidth = pageWidth - hintGutterWidth; + const int hintGutterHeight = isPortraitInverted ? 50 : 0; + const int contentY = hintGutterHeight; // Title const std::string truncTitle = - renderer.truncatedText(UI_12_FONT_ID, title.c_str(), pageWidth - 40, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_12_FONT_ID, 15, truncTitle.c_str(), true, EpdFontFamily::BOLD); + renderer.truncatedText(UI_12_FONT_ID, title.c_str(), contentWidth - 40, EpdFontFamily::BOLD); + 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); // Menu Items - constexpr int startY = 60; + const int startY = 60 + contentY; constexpr int lineHeight = 30; for (size_t i = 0; i < menuItems.size(); ++i) { @@ -95,15 +106,15 @@ void EpubReaderMenuActivity::renderScreen() { const bool isSelected = (static_cast(i) == selectedIndex); if (isSelected) { - renderer.fillRect(0, displayY, pageWidth - 1, lineHeight, true); + renderer.fillRect(contentX, displayY, contentWidth - 1, lineHeight, true); } - renderer.drawText(UI_10_FONT_ID, 20, displayY, menuItems[i].label.c_str(), !isSelected); + renderer.drawText(UI_10_FONT_ID, contentX + 20, displayY, menuItems[i].label.c_str(), !isSelected); if (menuItems[i].action == MenuAction::ROTATE_SCREEN) { const auto value = orientationLabels[pendingOrientation]; const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); - renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, displayY, value, !isSelected); + renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected); } } From fd53c14a311fde2846f0337d47c6fa7373d632cc Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Tue, 3 Feb 2026 18:25:26 +0300 Subject: [PATCH 4/5] 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(); } From 3d92bac3ec8e4688324061c03b570638926eea8f Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Tue, 3 Feb 2026 18:32:08 +0300 Subject: [PATCH 5/5] clang format fix --- src/activities/reader/EpubReaderActivity.cpp | 4 ++-- .../reader/EpubReaderChapterSelectionActivity.cpp | 11 ++++++----- src/activities/reader/EpubReaderMenuActivity.cpp | 4 ++-- src/activities/reader/EpubReaderMenuActivity.h | 3 +-- .../reader/XtcReaderChapterSelectionActivity.cpp | 13 ++++++------- 5 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 6c064aa6..a510f8f3 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -136,8 +136,8 @@ void EpubReaderActivity::loop() { xSemaphoreTake(renderingMutex, portMAX_DELAY); exitActivity(); enterNewActivity(new EpubReaderMenuActivity( - this->renderer, this->mappedInput, epub->getTitle(), SETTINGS.orientation, - [this](const uint8_t orientation) { onReaderMenuBack(orientation); }, + this->renderer, this->mappedInput, epub->getTitle(), SETTINGS.orientation, + [this](const uint8_t orientation) { onReaderMenuBack(orientation); }, [this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); })); xSemaphoreGive(renderingMutex); } diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index c47db235..932a4812 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -1,8 +1,9 @@ #include "EpubReaderChapterSelectionActivity.h" -#include #include +#include + #include "KOReaderCredentialStore.h" #include "KOReaderSyncActivity.h" #include "MappedInputManager.h" @@ -193,8 +194,8 @@ void EpubReaderChapterSelectionActivity::renderScreen() { const int totalItems = getTotalItems(); // Manual centering to honor content gutters. - const int titleX = contentX + - (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, "Go to Chapter", EpdFontFamily::BOLD)) / 2; + 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; @@ -214,8 +215,8 @@ void EpubReaderChapterSelectionActivity::renderScreen() { const int tocIndex = tocIndexFromItemIndex(itemIndex); auto item = epub->getTocItem(tocIndex); - // Indent per TOC level while keeping content within the gutter-safe region. - const int indentSize = contentX + 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(), contentWidth - 40 - indentSize); diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 8d80bbc0..25e3397a 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -101,8 +101,8 @@ void EpubReaderMenuActivity::renderScreen() { 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; + 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); // Menu Items diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 9f25d75d..7e783ad0 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -16,8 +16,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { enum class MenuAction { SELECT_CHAPTER, ROTATE_SCREEN, GO_HOME, DELETE_CACHE }; explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, - const uint8_t currentOrientation, - const std::function& onBack, + const uint8_t currentOrientation, const std::function& onBack, const std::function& onAction) : ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput), title(title), diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index a67e0acc..90d81247 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -1,8 +1,9 @@ #include "XtcReaderChapterSelectionActivity.h" -#include #include +#include + #include "MappedInputManager.h" #include "fontIds.h" @@ -146,15 +147,14 @@ void XtcReaderChapterSelectionActivity::renderScreen() { const int contentY = hintGutterHeight; const int pageItems = getPageItems(); // Manual centering to honor content gutters. - const int titleX = contentX + - (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, "Select Chapter", EpdFontFamily::BOLD)) / 2; + 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()) { // Center the empty state within the gutter-safe content region. - const int emptyX = - contentX + (contentWidth - renderer.getTextWidth(UI_10_FONT_ID, "No chapters")) / 2; + 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; @@ -166,8 +166,7 @@ void XtcReaderChapterSelectionActivity::renderScreen() { 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, contentX + 20, 60 + contentY + (i % pageItems) * 30, title, - i != selectorIndex); + renderer.drawText(UI_10_FONT_ID, contentX + 20, 60 + contentY + (i % pageItems) * 30, title, i != selectorIndex); } const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down");