mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-05 23:27:38 +03:00
fix: eliminate e-ink ghosting on menu navigation
Problem: E-ink display showed significant ghosting when navigating between menu screens. The HALF_REFRESH LUT was not applied consistently when entering or re-entering menu activities. Root Cause: 1. HALF_REFRESH only triggered on initial activity creation, not re-entry 2. Race conditions between main loop and display task 3. Empty directory early return bypassed HALF_REFRESH logic Solution: - FileSelectionActivity: Track lastRenderedPath to detect directory changes Use HALF_REFRESH when path differs. Mutex-protect updateRequired writes. - Other menus: Reset isFirstRender in onEnter() for HALF_REFRESH on entry UI improvements: - FileSelectionActivity: Header shows Browse/path, improved empty state - XtcReaderChapterSelectionActivity: Layout consistency, text truncation - EpubReaderChapterSelectionActivity: Text truncation and button hints
This commit is contained in:
parent
73959ea9ac
commit
768f2bd815
@ -18,6 +18,7 @@ int HomeActivity::getMenuItemCount() const { return hasContinueReading ? 4 : 3;
|
|||||||
|
|
||||||
void HomeActivity::onEnter() {
|
void HomeActivity::onEnter() {
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
|
isFirstRender = true;
|
||||||
|
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
@ -318,5 +319,10 @@ void HomeActivity::render() const {
|
|||||||
|
|
||||||
ScreenComponents::drawBattery(renderer, margin, pageHeight - 68);
|
ScreenComponents::drawBattery(renderer, margin, pageHeight - 68);
|
||||||
|
|
||||||
renderer.displayBuffer();
|
if (isFirstRender) {
|
||||||
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||||
|
isFirstRender = false;
|
||||||
|
} else {
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ class HomeActivity final : public Activity {
|
|||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
mutable bool isFirstRender = true;
|
||||||
bool hasContinueReading = false;
|
bool hasContinueReading = false;
|
||||||
std::string lastBookTitle;
|
std::string lastBookTitle;
|
||||||
std::string lastBookAuthor;
|
std::string lastBookAuthor;
|
||||||
|
|||||||
@ -34,6 +34,7 @@ void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) {
|
|||||||
|
|
||||||
void EpubReaderChapterSelectionActivity::onEnter() {
|
void EpubReaderChapterSelectionActivity::onEnter() {
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
|
isFirstRender = true;
|
||||||
|
|
||||||
if (!epub) {
|
if (!epub) {
|
||||||
return;
|
return;
|
||||||
@ -139,9 +140,20 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
|
|||||||
tocIndex++) {
|
tocIndex++) {
|
||||||
auto item = epub->getTocItem(tocIndex);
|
auto item = epub->getTocItem(tocIndex);
|
||||||
const int indentPx = (item.level - 1) * 12;
|
const int indentPx = (item.level - 1) * 12;
|
||||||
|
const auto truncatedTitle = renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(),
|
||||||
|
pageWidth - horizontalMargin * 2 - 8 - indentPx);
|
||||||
renderer.drawText(UI_10_FONT_ID, horizontalMargin + 4 + indentPx, listStartY + (tocIndex % pageItems) * rowHeight,
|
renderer.drawText(UI_10_FONT_ID, horizontalMargin + 4 + indentPx, listStartY + (tocIndex % pageItems) * rowHeight,
|
||||||
item.title.c_str(), tocIndex != selectorIndex);
|
truncatedTitle.c_str(), tocIndex != selectorIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.displayBuffer();
|
// Draw button hints
|
||||||
|
const auto labels = mappedInput.mapLabels("« Back", "Go", "", "");
|
||||||
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
|
if (isFirstRender) {
|
||||||
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||||
|
isFirstRender = false;
|
||||||
|
} else {
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ class EpubReaderChapterSelectionActivity final : public Activity {
|
|||||||
int currentSpineIndex = 0;
|
int currentSpineIndex = 0;
|
||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
mutable bool isFirstRender = true;
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
const std::function<void(int newSpineIndex)> onSelectSpineIndex;
|
const std::function<void(int newSpineIndex)> onSelectSpineIndex;
|
||||||
|
|
||||||
|
|||||||
@ -11,8 +11,8 @@ constexpr int PAGE_ITEMS = 20;
|
|||||||
constexpr int SKIP_PAGE_MS = 700;
|
constexpr int SKIP_PAGE_MS = 700;
|
||||||
constexpr unsigned long GO_HOME_MS = 1000;
|
constexpr unsigned long GO_HOME_MS = 1000;
|
||||||
constexpr int headerY = 16;
|
constexpr int headerY = 16;
|
||||||
constexpr int separatorY = 42;
|
constexpr int separatorY = 48;
|
||||||
constexpr int listStartY = 54;
|
constexpr int listStartY = 60;
|
||||||
constexpr int rowHeight = 28;
|
constexpr int rowHeight = 28;
|
||||||
constexpr int horizontalMargin = 16;
|
constexpr int horizontalMargin = 16;
|
||||||
} // namespace
|
} // namespace
|
||||||
@ -70,6 +70,7 @@ void FileSelectionActivity::loadFiles() {
|
|||||||
|
|
||||||
void FileSelectionActivity::onEnter() {
|
void FileSelectionActivity::onEnter() {
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
|
lastRenderedPath.clear(); // Force HALF_REFRESH on first render
|
||||||
|
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
@ -107,8 +108,10 @@ void FileSelectionActivity::loop() {
|
|||||||
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) {
|
if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) {
|
||||||
if (basepath != "/") {
|
if (basepath != "/") {
|
||||||
basepath = "/";
|
basepath = "/";
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
loadFiles();
|
loadFiles();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -128,8 +131,10 @@ void FileSelectionActivity::loop() {
|
|||||||
if (basepath.back() != '/') basepath += "/";
|
if (basepath.back() != '/') basepath += "/";
|
||||||
if (files[selectorIndex].back() == '/') {
|
if (files[selectorIndex].back() == '/') {
|
||||||
basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1);
|
basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1);
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
loadFiles();
|
loadFiles();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
} else {
|
} else {
|
||||||
onSelect(basepath + files[selectorIndex]);
|
onSelect(basepath + files[selectorIndex]);
|
||||||
}
|
}
|
||||||
@ -139,8 +144,10 @@ void FileSelectionActivity::loop() {
|
|||||||
if (basepath != "/") {
|
if (basepath != "/") {
|
||||||
basepath.replace(basepath.find_last_of('/'), std::string::npos, "");
|
basepath.replace(basepath.find_last_of('/'), std::string::npos, "");
|
||||||
if (basepath.empty()) basepath = "/";
|
if (basepath.empty()) basepath = "/";
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
loadFiles();
|
loadFiles();
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
} else {
|
} else {
|
||||||
onGoHome();
|
onGoHome();
|
||||||
}
|
}
|
||||||
@ -151,25 +158,29 @@ void FileSelectionActivity::loop() {
|
|||||||
} else {
|
} else {
|
||||||
selectorIndex = (selectorIndex + files.size() - 1) % files.size();
|
selectorIndex = (selectorIndex + files.size() - 1) % files.size();
|
||||||
}
|
}
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
} else if (nextReleased) {
|
} else if (nextReleased) {
|
||||||
if (skipPage) {
|
if (skipPage) {
|
||||||
selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % files.size();
|
selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % files.size();
|
||||||
} else {
|
} else {
|
||||||
selectorIndex = (selectorIndex + 1) % files.size();
|
selectorIndex = (selectorIndex + 1) % files.size();
|
||||||
}
|
}
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FileSelectionActivity::displayTaskLoop() {
|
void FileSelectionActivity::displayTaskLoop() {
|
||||||
while (true) {
|
while (true) {
|
||||||
|
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||||
if (updateRequired) {
|
if (updateRequired) {
|
||||||
updateRequired = false;
|
updateRequired = false;
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
render();
|
render();
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
}
|
}
|
||||||
|
xSemaphoreGive(renderingMutex);
|
||||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -179,8 +190,11 @@ void FileSelectionActivity::render() const {
|
|||||||
|
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
|
|
||||||
// Draw header
|
// Draw header with path
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, headerY, "Books", true, EpdFontFamily::BOLD);
|
const std::string pathDisplay = basepath == "/" ? "Browse" : basepath;
|
||||||
|
const auto truncatedPath = renderer.truncatedText(UI_12_FONT_ID, pathDisplay.c_str(),
|
||||||
|
pageWidth - horizontalMargin * 2, EpdFontFamily::BOLD);
|
||||||
|
renderer.drawCenteredText(UI_12_FONT_ID, headerY, truncatedPath.c_str(), true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
// Subtle separator line under header
|
// Subtle separator line under header
|
||||||
renderer.drawLine(horizontalMargin, separatorY, pageWidth - horizontalMargin, separatorY);
|
renderer.drawLine(horizontalMargin, separatorY, pageWidth - horizontalMargin, separatorY);
|
||||||
@ -190,8 +204,16 @@ void FileSelectionActivity::render() const {
|
|||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
if (files.empty()) {
|
if (files.empty()) {
|
||||||
renderer.drawText(UI_10_FONT_ID, horizontalMargin + 4, listStartY, "No books found");
|
const int emptyY = listStartY + 40;
|
||||||
renderer.displayBuffer();
|
renderer.drawCenteredText(UI_10_FONT_ID, emptyY, "No files found");
|
||||||
|
renderer.drawCenteredText(SMALL_FONT_ID, emptyY + 24, "Supported: .epub, .xtc, .xtch");
|
||||||
|
// Use HALF_REFRESH when directory changed
|
||||||
|
if (basepath != lastRenderedPath) {
|
||||||
|
lastRenderedPath = basepath;
|
||||||
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||||
|
} else {
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -203,5 +225,11 @@ void FileSelectionActivity::render() const {
|
|||||||
i != selectorIndex);
|
i != selectorIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.displayBuffer();
|
// Use HALF_REFRESH when directory changed (basepath differs from last render)
|
||||||
|
if (basepath != lastRenderedPath) {
|
||||||
|
lastRenderedPath = basepath;
|
||||||
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||||
|
} else {
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ class FileSelectionActivity final : public Activity {
|
|||||||
std::vector<std::string> files;
|
std::vector<std::string> files;
|
||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
mutable std::string lastRenderedPath;
|
||||||
const std::function<void(const std::string&)> onSelect;
|
const std::function<void(const std::string&)> onSelect;
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,7 @@ void XtcReaderChapterSelectionActivity::taskTrampoline(void* param) {
|
|||||||
|
|
||||||
void XtcReaderChapterSelectionActivity::onEnter() {
|
void XtcReaderChapterSelectionActivity::onEnter() {
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
|
isFirstRender = true;
|
||||||
|
|
||||||
if (!xtc) {
|
if (!xtc) {
|
||||||
return;
|
return;
|
||||||
@ -130,22 +131,49 @@ void XtcReaderChapterSelectionActivity::renderScreen() {
|
|||||||
|
|
||||||
const auto pageWidth = renderer.getScreenWidth();
|
const auto pageWidth = renderer.getScreenWidth();
|
||||||
const int pageItems = getPageItems();
|
const int pageItems = getPageItems();
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Select Chapter", true, EpdFontFamily::BOLD);
|
|
||||||
|
// Layout constants matching other screens
|
||||||
|
constexpr int headerY = 16;
|
||||||
|
constexpr int separatorY = 42;
|
||||||
|
constexpr int listStartY = 54;
|
||||||
|
constexpr int rowHeight = 28;
|
||||||
|
constexpr int horizontalMargin = 16;
|
||||||
|
|
||||||
|
// Draw header
|
||||||
|
renderer.drawCenteredText(UI_12_FONT_ID, headerY, "Chapters", true, EpdFontFamily::BOLD);
|
||||||
|
|
||||||
|
// Subtle separator line under header
|
||||||
|
renderer.drawLine(horizontalMargin, separatorY, pageWidth - horizontalMargin, separatorY);
|
||||||
|
|
||||||
const auto& chapters = xtc->getChapters();
|
const auto& chapters = xtc->getChapters();
|
||||||
if (chapters.empty()) {
|
if (chapters.empty()) {
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 120, "No chapters");
|
const int emptyY = listStartY + 40;
|
||||||
|
renderer.drawCenteredText(UI_10_FONT_ID, emptyY, "No chapters found");
|
||||||
renderer.displayBuffer();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw selection highlight
|
||||||
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
const auto pageStartIndex = selectorIndex / pageItems * pageItems;
|
||||||
renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30);
|
renderer.fillRect(0, listStartY + (selectorIndex % pageItems) * rowHeight - 2, pageWidth - 1, rowHeight);
|
||||||
|
|
||||||
|
// Draw chapter list
|
||||||
for (int i = pageStartIndex; i < static_cast<int>(chapters.size()) && i < pageStartIndex + pageItems; i++) {
|
for (int i = pageStartIndex; i < static_cast<int>(chapters.size()) && i < pageStartIndex + pageItems; i++) {
|
||||||
const auto& chapter = chapters[i];
|
const auto& chapter = chapters[i];
|
||||||
const char* title = chapter.name.empty() ? "Unnamed" : chapter.name.c_str();
|
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);
|
const auto truncatedTitle = renderer.truncatedText(UI_10_FONT_ID, title, pageWidth - horizontalMargin * 2 - 8);
|
||||||
|
renderer.drawText(UI_10_FONT_ID, horizontalMargin + 4, listStartY + (i % pageItems) * rowHeight,
|
||||||
|
truncatedTitle.c_str(), i != selectorIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.displayBuffer();
|
// Draw button hints
|
||||||
|
const auto labels = mappedInput.mapLabels("« Back", "Go", "", "");
|
||||||
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
|
if (isFirstRender) {
|
||||||
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||||
|
isFirstRender = false;
|
||||||
|
} else {
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ class XtcReaderChapterSelectionActivity final : public Activity {
|
|||||||
uint32_t currentPage = 0;
|
uint32_t currentPage = 0;
|
||||||
int selectorIndex = 0;
|
int selectorIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
mutable bool isFirstRender = true;
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
const std::function<void(uint32_t newPage)> onSelectPage;
|
const std::function<void(uint32_t newPage)> onSelectPage;
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,7 @@ void SettingsActivity::taskTrampoline(void* param) {
|
|||||||
|
|
||||||
void SettingsActivity::onEnter() {
|
void SettingsActivity::onEnter() {
|
||||||
Activity::onEnter();
|
Activity::onEnter();
|
||||||
|
isFirstRender = true;
|
||||||
|
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
@ -215,6 +216,11 @@ void SettingsActivity::render() const {
|
|||||||
const auto labels = mappedInput.mapLabels("« Save", "Toggle", "", "");
|
const auto labels = mappedInput.mapLabels("« Save", "Toggle", "", "");
|
||||||
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||||
|
|
||||||
// Always use standard refresh for settings screen
|
// Use HALF_REFRESH on first render to clear ghosting, then FAST_REFRESH
|
||||||
renderer.displayBuffer();
|
if (isFirstRender) {
|
||||||
|
renderer.displayBuffer(EInkDisplay::HALF_REFRESH);
|
||||||
|
isFirstRender = false;
|
||||||
|
} else {
|
||||||
|
renderer.displayBuffer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ class SettingsActivity final : public ActivityWithSubactivity {
|
|||||||
TaskHandle_t displayTaskHandle = nullptr;
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
SemaphoreHandle_t renderingMutex = nullptr;
|
SemaphoreHandle_t renderingMutex = nullptr;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
mutable bool isFirstRender = true;
|
||||||
int selectedSettingIndex = 0; // Currently selected setting
|
int selectedSettingIndex = 0; // Currently selected setting
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user