mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-06 23:57:39 +03:00
feat: Go To Position for epubs (#666)
## Summary * Adds Go To % action in Epub Reader menu with slider style percent selector <img width="860" height="1147" alt="image" src="https://github.com/user-attachments/assets/a38ecc71-429e-40e8-94ac-37fb1509dbd9" /> --- ### 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 >**_ --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
17fedd2a69
commit
ddbe49f536
@ -8,6 +8,7 @@
|
|||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "EpubReaderChapterSelectionActivity.h"
|
#include "EpubReaderChapterSelectionActivity.h"
|
||||||
|
#include "EpubReaderPercentSelectionActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
@ -20,6 +21,16 @@ constexpr unsigned long goHomeMs = 1000;
|
|||||||
constexpr int statusBarMargin = 19;
|
constexpr int statusBarMargin = 19;
|
||||||
constexpr int progressBarMarginTop = 1;
|
constexpr int progressBarMarginTop = 1;
|
||||||
|
|
||||||
|
int clampPercent(int percent) {
|
||||||
|
if (percent < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (percent > 100) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
return percent;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply the logical reader orientation to the renderer.
|
// Apply the logical reader orientation to the renderer.
|
||||||
// This centralizes orientation mapping so we don't duplicate switch logic elsewhere.
|
// This centralizes orientation mapping so we don't duplicate switch logic elsewhere.
|
||||||
void applyReaderOrientation(GfxRenderer& renderer, const uint8_t orientation) {
|
void applyReaderOrientation(GfxRenderer& renderer, const uint8_t orientation) {
|
||||||
@ -132,14 +143,22 @@ void EpubReaderActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enter chapter selection activity
|
// Enter reader menu activity.
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
// Don't start activity transition while rendering
|
// Don't start activity transition while rendering
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
const int currentPage = section ? section->currentPage + 1 : 0;
|
||||||
|
const int totalPages = section ? section->pageCount : 0;
|
||||||
|
float bookProgress = 0.0f;
|
||||||
|
if (epub && epub->getBookSize() > 0 && section && section->pageCount > 0) {
|
||||||
|
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
|
||||||
|
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
||||||
|
}
|
||||||
|
const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
||||||
exitActivity();
|
exitActivity();
|
||||||
enterNewActivity(new EpubReaderMenuActivity(
|
enterNewActivity(new EpubReaderMenuActivity(
|
||||||
this->renderer, this->mappedInput, epub->getTitle(), SETTINGS.orientation,
|
this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
|
||||||
[this](const uint8_t orientation) { onReaderMenuBack(orientation); },
|
SETTINGS.orientation, [this](const uint8_t orientation) { onReaderMenuBack(orientation); },
|
||||||
[this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); }));
|
[this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); }));
|
||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
}
|
}
|
||||||
@ -236,6 +255,68 @@ void EpubReaderActivity::onReaderMenuBack(const uint8_t orientation) {
|
|||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Translate an absolute percent into a spine index plus a normalized position
|
||||||
|
// within that spine so we can jump after the section is loaded.
|
||||||
|
void EpubReaderActivity::jumpToPercent(int percent) {
|
||||||
|
if (!epub) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t bookSize = epub->getBookSize();
|
||||||
|
if (bookSize == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize input to 0-100 to avoid invalid jumps.
|
||||||
|
percent = clampPercent(percent);
|
||||||
|
|
||||||
|
// Convert percent into a byte-like absolute position across the spine sizes.
|
||||||
|
// Use an overflow-safe computation: (bookSize / 100) * percent + (bookSize % 100) * percent / 100
|
||||||
|
size_t targetSize =
|
||||||
|
(bookSize / 100) * static_cast<size_t>(percent) + (bookSize % 100) * static_cast<size_t>(percent) / 100;
|
||||||
|
if (percent >= 100) {
|
||||||
|
// Ensure the final percent lands inside the last spine item.
|
||||||
|
targetSize = bookSize - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int spineCount = epub->getSpineItemsCount();
|
||||||
|
if (spineCount == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int targetSpineIndex = spineCount - 1;
|
||||||
|
size_t prevCumulative = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < spineCount; i++) {
|
||||||
|
const size_t cumulative = epub->getCumulativeSpineItemSize(i);
|
||||||
|
if (targetSize <= cumulative) {
|
||||||
|
// Found the spine item containing the absolute position.
|
||||||
|
targetSpineIndex = i;
|
||||||
|
prevCumulative = (i > 0) ? epub->getCumulativeSpineItemSize(i - 1) : 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t cumulative = epub->getCumulativeSpineItemSize(targetSpineIndex);
|
||||||
|
const size_t spineSize = (cumulative > prevCumulative) ? (cumulative - prevCumulative) : 0;
|
||||||
|
// Store a normalized position within the spine so it can be applied once loaded.
|
||||||
|
pendingSpineProgress =
|
||||||
|
(spineSize == 0) ? 0.0f : static_cast<float>(targetSize - prevCumulative) / static_cast<float>(spineSize);
|
||||||
|
if (pendingSpineProgress < 0.0f) {
|
||||||
|
pendingSpineProgress = 0.0f;
|
||||||
|
} else if (pendingSpineProgress > 1.0f) {
|
||||||
|
pendingSpineProgress = 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state so renderScreen() reloads and repositions on the target spine.
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
currentSpineIndex = targetSpineIndex;
|
||||||
|
nextPageNumber = 0;
|
||||||
|
pendingPercentJump = true;
|
||||||
|
section.reset();
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
}
|
||||||
|
|
||||||
void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) {
|
void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: {
|
case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: {
|
||||||
@ -279,6 +360,32 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
|||||||
xSemaphoreGive(renderingMutex);
|
xSemaphoreGive(renderingMutex);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case EpubReaderMenuActivity::MenuAction::GO_TO_PERCENT: {
|
||||||
|
// Launch the slider-based percent selector and return here on confirm/cancel.
|
||||||
|
float bookProgress = 0.0f;
|
||||||
|
if (epub && epub->getBookSize() > 0 && section && section->pageCount > 0) {
|
||||||
|
const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount);
|
||||||
|
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
|
||||||
|
}
|
||||||
|
const int initialPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
exitActivity();
|
||||||
|
enterNewActivity(new EpubReaderPercentSelectionActivity(
|
||||||
|
renderer, mappedInput, initialPercent,
|
||||||
|
[this](const int percent) {
|
||||||
|
// Apply the new position and exit back to the reader.
|
||||||
|
jumpToPercent(percent);
|
||||||
|
exitActivity();
|
||||||
|
updateRequired = true;
|
||||||
|
},
|
||||||
|
[this]() {
|
||||||
|
// Cancel selection and return to the reader.
|
||||||
|
exitActivity();
|
||||||
|
updateRequired = true;
|
||||||
|
}));
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
|
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
|
||||||
// 2. Trigger the reader's "Go Home" callback
|
// 2. Trigger the reader's "Go Home" callback
|
||||||
if (onGoHome) {
|
if (onGoHome) {
|
||||||
@ -437,6 +544,16 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
}
|
}
|
||||||
cachedChapterTotalPageCount = 0; // resets to 0 to prevent reading cached progress again
|
cachedChapterTotalPageCount = 0; // resets to 0 to prevent reading cached progress again
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pendingPercentJump && section->pageCount > 0) {
|
||||||
|
// Apply the pending percent jump now that we know the new section's page count.
|
||||||
|
int newPage = static_cast<int>(pendingSpineProgress * static_cast<float>(section->pageCount));
|
||||||
|
if (newPage >= section->pageCount) {
|
||||||
|
newPage = section->pageCount - 1;
|
||||||
|
}
|
||||||
|
section->currentPage = newPage;
|
||||||
|
pendingPercentJump = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
|
|||||||
@ -18,6 +18,11 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
|
|||||||
int pagesUntilFullRefresh = 0;
|
int pagesUntilFullRefresh = 0;
|
||||||
int cachedSpineIndex = 0;
|
int cachedSpineIndex = 0;
|
||||||
int cachedChapterTotalPageCount = 0;
|
int cachedChapterTotalPageCount = 0;
|
||||||
|
// Signals that the next render should reposition within the newly loaded section
|
||||||
|
// based on a cross-book percentage jump.
|
||||||
|
bool pendingPercentJump = false;
|
||||||
|
// Normalized 0.0-1.0 progress within the target spine item, computed from book percentage.
|
||||||
|
float pendingSpineProgress = 0.0f;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
@ -29,6 +34,8 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
|
|||||||
int orientedMarginBottom, int orientedMarginLeft);
|
int orientedMarginBottom, int orientedMarginLeft);
|
||||||
void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const;
|
void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const;
|
||||||
void saveProgress(int spineIndex, int currentPage, int pageCount);
|
void saveProgress(int spineIndex, int currentPage, int pageCount);
|
||||||
|
// Jump to a percentage of the book (0-100), mapping it to spine and page.
|
||||||
|
void jumpToPercent(int percent);
|
||||||
void onReaderMenuBack(uint8_t orientation);
|
void onReaderMenuBack(uint8_t orientation);
|
||||||
void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action);
|
void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action);
|
||||||
void applyOrientation(uint8_t orientation);
|
void applyOrientation(uint8_t orientation);
|
||||||
|
|||||||
@ -106,8 +106,16 @@ void EpubReaderMenuActivity::renderScreen() {
|
|||||||
contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, truncTitle.c_str(), EpdFontFamily::BOLD)) / 2;
|
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);
|
renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, truncTitle.c_str(), true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
// Progress summary
|
||||||
|
std::string progressLine;
|
||||||
|
if (totalPages > 0) {
|
||||||
|
progressLine = "Chapter: " + std::to_string(currentPage) + "/" + std::to_string(totalPages) + " pages | ";
|
||||||
|
}
|
||||||
|
progressLine += "Book: " + std::to_string(bookProgressPercent) + "%";
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, 45, progressLine.c_str());
|
||||||
|
|
||||||
// Menu Items
|
// Menu Items
|
||||||
const int startY = 60 + contentY;
|
const int startY = 75 + contentY;
|
||||||
constexpr int lineHeight = 30;
|
constexpr int lineHeight = 30;
|
||||||
|
|
||||||
for (size_t i = 0; i < menuItems.size(); ++i) {
|
for (size_t i = 0; i < menuItems.size(); ++i) {
|
||||||
|
|||||||
@ -13,14 +13,19 @@
|
|||||||
|
|
||||||
class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
||||||
public:
|
public:
|
||||||
enum class MenuAction { SELECT_CHAPTER, ROTATE_SCREEN, GO_HOME, DELETE_CACHE };
|
// Menu actions available from the reader menu.
|
||||||
|
enum class MenuAction { SELECT_CHAPTER, GO_TO_PERCENT, ROTATE_SCREEN, GO_HOME, DELETE_CACHE };
|
||||||
|
|
||||||
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
||||||
|
const int currentPage, const int totalPages, const int bookProgressPercent,
|
||||||
const uint8_t currentOrientation, const std::function<void(uint8_t)>& onBack,
|
const uint8_t currentOrientation, const std::function<void(uint8_t)>& onBack,
|
||||||
const std::function<void(MenuAction)>& onAction)
|
const std::function<void(MenuAction)>& onAction)
|
||||||
: ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput),
|
: ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput),
|
||||||
title(title),
|
title(title),
|
||||||
pendingOrientation(currentOrientation),
|
pendingOrientation(currentOrientation),
|
||||||
|
currentPage(currentPage),
|
||||||
|
totalPages(totalPages),
|
||||||
|
bookProgressPercent(bookProgressPercent),
|
||||||
onBack(onBack),
|
onBack(onBack),
|
||||||
onAction(onAction) {}
|
onAction(onAction) {}
|
||||||
|
|
||||||
@ -34,8 +39,10 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
|||||||
std::string label;
|
std::string label;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fixed menu layout (order matters for up/down navigation).
|
||||||
const std::vector<MenuItem> menuItems = {{MenuAction::SELECT_CHAPTER, "Go to Chapter"},
|
const std::vector<MenuItem> menuItems = {{MenuAction::SELECT_CHAPTER, "Go to Chapter"},
|
||||||
{MenuAction::ROTATE_SCREEN, "Reading Orientation"},
|
{MenuAction::ROTATE_SCREEN, "Reading Orientation"},
|
||||||
|
{MenuAction::GO_TO_PERCENT, "Go to %"},
|
||||||
{MenuAction::GO_HOME, "Go Home"},
|
{MenuAction::GO_HOME, "Go Home"},
|
||||||
{MenuAction::DELETE_CACHE, "Delete Book Cache"}};
|
{MenuAction::DELETE_CACHE, "Delete Book Cache"}};
|
||||||
|
|
||||||
@ -46,6 +53,9 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
|||||||
std::string title = "Reader Menu";
|
std::string title = "Reader Menu";
|
||||||
uint8_t pendingOrientation = 0;
|
uint8_t pendingOrientation = 0;
|
||||||
const std::vector<const char*> orientationLabels = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"};
|
const std::vector<const char*> orientationLabels = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"};
|
||||||
|
int currentPage = 0;
|
||||||
|
int totalPages = 0;
|
||||||
|
int bookProgressPercent = 0;
|
||||||
|
|
||||||
const std::function<void(uint8_t)> onBack;
|
const std::function<void(uint8_t)> onBack;
|
||||||
const std::function<void(MenuAction)> onAction;
|
const std::function<void(MenuAction)> onAction;
|
||||||
|
|||||||
139
src/activities/reader/EpubReaderPercentSelectionActivity.cpp
Normal file
139
src/activities/reader/EpubReaderPercentSelectionActivity.cpp
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
#include "EpubReaderPercentSelectionActivity.h"
|
||||||
|
|
||||||
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "components/UITheme.h"
|
||||||
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Fine/coarse slider step sizes for percent adjustments.
|
||||||
|
constexpr int kSmallStep = 1;
|
||||||
|
constexpr int kLargeStep = 10;
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void EpubReaderPercentSelectionActivity::onEnter() {
|
||||||
|
ActivityWithSubactivity::onEnter();
|
||||||
|
// Set up rendering task and mark first frame dirty.
|
||||||
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
updateRequired = true;
|
||||||
|
xTaskCreate(&EpubReaderPercentSelectionActivity::taskTrampoline, "EpubPercentSlider", 4096, this, 1,
|
||||||
|
&displayTaskHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderPercentSelectionActivity::onExit() {
|
||||||
|
ActivityWithSubactivity::onExit();
|
||||||
|
// Ensure the render task is stopped before freeing the mutex.
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
if (displayTaskHandle) {
|
||||||
|
vTaskDelete(displayTaskHandle);
|
||||||
|
displayTaskHandle = nullptr;
|
||||||
|
}
|
||||||
|
vSemaphoreDelete(renderingMutex);
|
||||||
|
renderingMutex = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderPercentSelectionActivity::taskTrampoline(void* param) {
|
||||||
|
auto* self = static_cast<EpubReaderPercentSelectionActivity*>(param);
|
||||||
|
self->displayTaskLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderPercentSelectionActivity::displayTaskLoop() {
|
||||||
|
while (true) {
|
||||||
|
// Render only when the view is dirty and no subactivity is running.
|
||||||
|
if (updateRequired && !subActivity) {
|
||||||
|
updateRequired = false;
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
|
renderScreen();
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
|
}
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) {
|
||||||
|
// Apply delta and clamp within 0-100.
|
||||||
|
percent += delta;
|
||||||
|
if (percent < 0) {
|
||||||
|
percent = 0;
|
||||||
|
} else if (percent > 100) {
|
||||||
|
percent = 100;
|
||||||
|
}
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderPercentSelectionActivity::loop() {
|
||||||
|
if (subActivity) {
|
||||||
|
subActivity->loop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back cancels, confirm selects, arrows adjust the percent.
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||||
|
onCancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
|
onSelect(percent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Left)) {
|
||||||
|
adjustPercent(-kSmallStep);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
|
||||||
|
adjustPercent(kSmallStep);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Up)) {
|
||||||
|
adjustPercent(kLargeStep);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mappedInput.wasReleased(MappedInputManager::Button::Down)) {
|
||||||
|
adjustPercent(-kLargeStep);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderPercentSelectionActivity::renderScreen() {
|
||||||
|
renderer.clearScreen();
|
||||||
|
|
||||||
|
// Title and numeric percent value.
|
||||||
|
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Go to Position", true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
const std::string percentText = std::to_string(percent) + "%";
|
||||||
|
renderer.drawCenteredText(UI_12_FONT_ID, 90, percentText.c_str(), true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
// Draw slider track.
|
||||||
|
const int screenWidth = renderer.getScreenWidth();
|
||||||
|
constexpr int barWidth = 360;
|
||||||
|
constexpr int barHeight = 16;
|
||||||
|
const int barX = (screenWidth - barWidth) / 2;
|
||||||
|
const int barY = 140;
|
||||||
|
|
||||||
|
renderer.drawRect(barX, barY, barWidth, barHeight);
|
||||||
|
|
||||||
|
// Fill slider based on percent.
|
||||||
|
const int fillWidth = (barWidth - 4) * percent / 100;
|
||||||
|
if (fillWidth > 0) {
|
||||||
|
renderer.fillRect(barX + 2, barY + 2, fillWidth, barHeight - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a simple knob centered at the current percent.
|
||||||
|
const int knobX = barX + 2 + fillWidth - 2;
|
||||||
|
renderer.fillRect(knobX, barY - 4, 4, barHeight + 8, true);
|
||||||
|
|
||||||
|
// Hint text for step sizes.
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, barY + 30, "Left/Right: 1% Up/Down: 10%", true);
|
||||||
|
|
||||||
|
// Button hints follow the current front button layout.
|
||||||
|
const auto labels = mappedInput.mapLabels("« Back", "Select", "-", "+");
|
||||||
|
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
46
src/activities/reader/EpubReaderPercentSelectionActivity.h
Normal file
46
src/activities/reader/EpubReaderPercentSelectionActivity.h
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include "MappedInputManager.h"
|
||||||
|
#include "activities/ActivityWithSubactivity.h"
|
||||||
|
|
||||||
|
class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity {
|
||||||
|
public:
|
||||||
|
// Slider-style percent selector for jumping within a book.
|
||||||
|
explicit EpubReaderPercentSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
|
const int initialPercent, const std::function<void(int)>& onSelect,
|
||||||
|
const std::function<void()>& onCancel)
|
||||||
|
: ActivityWithSubactivity("EpubReaderPercentSelection", renderer, mappedInput),
|
||||||
|
percent(initialPercent),
|
||||||
|
onSelect(onSelect),
|
||||||
|
onCancel(onCancel) {}
|
||||||
|
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void loop() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Current percent value (0-100) shown on the slider.
|
||||||
|
int percent = 0;
|
||||||
|
// Render dirty flag for the task loop.
|
||||||
|
bool updateRequired = false;
|
||||||
|
// FreeRTOS task and mutex for rendering.
|
||||||
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
|
|
||||||
|
// Callback invoked when the user confirms a percent.
|
||||||
|
const std::function<void(int)> onSelect;
|
||||||
|
// Callback invoked when the user cancels the slider.
|
||||||
|
const std::function<void()> onCancel;
|
||||||
|
|
||||||
|
static void taskTrampoline(void* param);
|
||||||
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
// Render the slider UI.
|
||||||
|
void renderScreen();
|
||||||
|
// Change the current percent by a delta and clamp within bounds.
|
||||||
|
void adjustPercent(int delta);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user