mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-05 15:17:37 +03:00
feat: quick rotate option in epub reader menu (#685)
## Summary * adds rotation setting in epub reader menu, actual rotation happens on going back * improves button hint drawing to draw correctly in all orientations <img width="860" height="1147" alt="image" src="https://github.com/user-attachments/assets/91ceeca6-729f-4304-b68a-e412f6e2c9a7" /> --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< PARTIALLY >**_
This commit is contained in:
parent
23ecc52261
commit
ee987f07ff
@ -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();
|
||||
|
||||
@ -129,11 +136,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);
|
||||
}
|
||||
@ -222,8 +228,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;
|
||||
}
|
||||
|
||||
@ -305,6 +314,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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "KOReaderCredentialStore.h"
|
||||
#include "KOReaderSyncActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
@ -35,20 +37,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) {
|
||||
@ -179,39 +179,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");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
}
|
||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
|
||||
@ -57,9 +57,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);
|
||||
@ -67,7 +74,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
|
||||
}
|
||||
}
|
||||
@ -75,14 +83,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) {
|
||||
@ -90,10 +115,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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
@ -11,18 +13,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 {
|
||||
@ -132,22 +134,40 @@ 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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user