This commit is contained in:
Arthur Tazhitdinov 2026-02-03 18:33:15 +03:00 committed by GitHub
commit 46483f4a35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 177 additions and 71 deletions

View File

@ -20,22 +20,10 @@ constexpr unsigned long goHomeMs = 1000;
constexpr int statusBarMargin = 19;
constexpr int progressBarMarginTop = 1;
} // namespace
void EpubReaderActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderActivity*>(param);
self->displayTaskLoop();
}
void EpubReaderActivity::onEnter() {
ActivityWithSubactivity::onEnter();
if (!epub) {
return;
}
// Configure screen orientation based on settings
switch (SETTINGS.orientation) {
// Apply the logical reader orientation to the renderer.
// This centralizes orientation mapping so we don't duplicate switch logic elsewhere.
void applyReaderOrientation(GfxRenderer& renderer, const uint8_t orientation) {
switch (orientation) {
case CrossPointSettings::ORIENTATION::PORTRAIT:
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
break;
@ -51,6 +39,25 @@ void EpubReaderActivity::onEnter() {
default:
break;
}
}
} // namespace
void EpubReaderActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderActivity*>(param);
self->displayTaskLoop();
}
void EpubReaderActivity::onEnter() {
ActivityWithSubactivity::onEnter();
if (!epub) {
return;
}
// 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();
@ -127,11 +134,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 +226,11 @@ void EpubReaderActivity::loop() {
}
}
void EpubReaderActivity::onReaderMenuBack() {
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;
}
@ -303,6 +312,32 @@ 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;
cachedChapterTotalPageCount = section->pageCount;
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);
}
void EpubReaderActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {

View File

@ -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> epub,

View File

@ -2,6 +2,8 @@
#include <GfxRenderer.h>
#include <algorithm>
#include "KOReaderCredentialStore.h"
#include "KOReaderSyncActivity.h"
#include "MappedInputManager.h"
@ -34,20 +36,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 +178,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();
}

View File

@ -56,9 +56,16 @@ 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) {
// Cycle orientation preview locally; actual rotation happens on menu exit.
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 +73,8 @@ void EpubReaderMenuActivity::loop() {
// 3. CRITICAL: Return immediately. 'this' is likely deleted now.
return;
} else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
onBack();
// Return the pending orientation to the parent so it can apply on exit.
onBack(pendingOrientation);
return; // Also return here just in case
}
}
@ -74,14 +82,31 @@ void EpubReaderMenuActivity::loop() {
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;
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);
// 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);
// Menu Items
constexpr int startY = 60;
const int startY = 60 + contentY;
constexpr int lineHeight = 30;
for (size_t i = 0; i < menuItems.size(); ++i) {
@ -89,10 +114,18 @@ void EpubReaderMenuActivity::renderScreen() {
const bool isSelected = (static_cast<int>(i) == selectedIndex);
if (isSelected) {
renderer.fillRect(0, displayY, pageWidth - 1, lineHeight, true);
// 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, 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) {
// 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);
}
}
// Footer / Hints

View File

@ -13,12 +13,14 @@
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<void()>& onBack, const std::function<void(MenuAction)>& onAction)
const uint8_t currentOrientation, const std::function<void(uint8_t)>& onBack,
const std::function<void(MenuAction)>& onAction)
: ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput),
title(title),
pendingOrientation(currentOrientation),
onBack(onBack),
onAction(onAction) {}
@ -33,6 +35,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
};
const std::vector<MenuItem> menuItems = {{MenuAction::SELECT_CHAPTER, "Go to Chapter"},
{MenuAction::ROTATE_SCREEN, "Reading Orientation"},
{MenuAction::GO_HOME, "Go Home"},
{MenuAction::DELETE_CACHE, "Delete Book Cache"}};
@ -41,8 +44,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<const char*> orientationLabels = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"};
const std::function<void()> onBack;
const std::function<void(uint8_t)> onBack;
const std::function<void(MenuAction)> onAction;
static void taskTrampoline(void* param);

View File

@ -2,6 +2,8 @@
#include <GfxRenderer.h>
#include <algorithm>
#include "MappedInputManager.h"
#include "fontIds.h"
@ -10,18 +12,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 +133,44 @@ 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<int>(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();
}