From 768f2bd8155ecaa4c483afffe7de590e07a82bff Mon Sep 17 00:00:00 2001 From: ratedcounsel Date: Wed, 31 Dec 2025 13:35:46 +0000 Subject: [PATCH] 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 --- src/activities/home/HomeActivity.cpp | 8 +++- src/activities/home/HomeActivity.h | 1 + .../EpubReaderChapterSelectionActivity.cpp | 16 ++++++- .../EpubReaderChapterSelectionActivity.h | 1 + .../reader/FileSelectionActivity.cpp | 46 +++++++++++++++---- src/activities/reader/FileSelectionActivity.h | 1 + .../XtcReaderChapterSelectionActivity.cpp | 38 +++++++++++++-- .../XtcReaderChapterSelectionActivity.h | 1 + src/activities/settings/SettingsActivity.cpp | 10 +++- src/activities/settings/SettingsActivity.h | 1 + 10 files changed, 104 insertions(+), 19 deletions(-) diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 898452f4..9ad065e7 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -18,6 +18,7 @@ int HomeActivity::getMenuItemCount() const { return hasContinueReading ? 4 : 3; void HomeActivity::onEnter() { Activity::onEnter(); + isFirstRender = true; renderingMutex = xSemaphoreCreateMutex(); @@ -318,5 +319,10 @@ void HomeActivity::render() const { ScreenComponents::drawBattery(renderer, margin, pageHeight - 68); - renderer.displayBuffer(); + if (isFirstRender) { + renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + isFirstRender = false; + } else { + renderer.displayBuffer(); + } } diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index b6c9767d..8c8df9c2 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -12,6 +12,7 @@ class HomeActivity final : public Activity { SemaphoreHandle_t renderingMutex = nullptr; int selectorIndex = 0; bool updateRequired = false; + mutable bool isFirstRender = true; bool hasContinueReading = false; std::string lastBookTitle; std::string lastBookAuthor; diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 6c73ee30..a45b2e71 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -34,6 +34,7 @@ void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) { void EpubReaderChapterSelectionActivity::onEnter() { Activity::onEnter(); + isFirstRender = true; if (!epub) { return; @@ -139,9 +140,20 @@ void EpubReaderChapterSelectionActivity::renderScreen() { tocIndex++) { auto item = epub->getTocItem(tocIndex); 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, - 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(); + } } diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.h b/src/activities/reader/EpubReaderChapterSelectionActivity.h index cf3f1905..75243bf6 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.h +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.h @@ -15,6 +15,7 @@ class EpubReaderChapterSelectionActivity final : public Activity { int currentSpineIndex = 0; int selectorIndex = 0; bool updateRequired = false; + mutable bool isFirstRender = true; const std::function onGoBack; const std::function onSelectSpineIndex; diff --git a/src/activities/reader/FileSelectionActivity.cpp b/src/activities/reader/FileSelectionActivity.cpp index 3045314d..cdcbbb6c 100644 --- a/src/activities/reader/FileSelectionActivity.cpp +++ b/src/activities/reader/FileSelectionActivity.cpp @@ -11,8 +11,8 @@ constexpr int PAGE_ITEMS = 20; constexpr int SKIP_PAGE_MS = 700; constexpr unsigned long GO_HOME_MS = 1000; constexpr int headerY = 16; -constexpr int separatorY = 42; -constexpr int listStartY = 54; +constexpr int separatorY = 48; +constexpr int listStartY = 60; constexpr int rowHeight = 28; constexpr int horizontalMargin = 16; } // namespace @@ -70,6 +70,7 @@ void FileSelectionActivity::loadFiles() { void FileSelectionActivity::onEnter() { Activity::onEnter(); + lastRenderedPath.clear(); // Force HALF_REFRESH on first render renderingMutex = xSemaphoreCreateMutex(); @@ -107,8 +108,10 @@ void FileSelectionActivity::loop() { if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) { if (basepath != "/") { basepath = "/"; + xSemaphoreTake(renderingMutex, portMAX_DELAY); loadFiles(); updateRequired = true; + xSemaphoreGive(renderingMutex); } return; } @@ -128,8 +131,10 @@ void FileSelectionActivity::loop() { if (basepath.back() != '/') basepath += "/"; if (files[selectorIndex].back() == '/') { basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1); + xSemaphoreTake(renderingMutex, portMAX_DELAY); loadFiles(); updateRequired = true; + xSemaphoreGive(renderingMutex); } else { onSelect(basepath + files[selectorIndex]); } @@ -139,8 +144,10 @@ void FileSelectionActivity::loop() { if (basepath != "/") { basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); if (basepath.empty()) basepath = "/"; + xSemaphoreTake(renderingMutex, portMAX_DELAY); loadFiles(); updateRequired = true; + xSemaphoreGive(renderingMutex); } else { onGoHome(); } @@ -151,25 +158,29 @@ void FileSelectionActivity::loop() { } else { selectorIndex = (selectorIndex + files.size() - 1) % files.size(); } + xSemaphoreTake(renderingMutex, portMAX_DELAY); updateRequired = true; + xSemaphoreGive(renderingMutex); } else if (nextReleased) { if (skipPage) { selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % files.size(); } else { selectorIndex = (selectorIndex + 1) % files.size(); } + xSemaphoreTake(renderingMutex, portMAX_DELAY); updateRequired = true; + xSemaphoreGive(renderingMutex); } } void FileSelectionActivity::displayTaskLoop() { while (true) { + xSemaphoreTake(renderingMutex, portMAX_DELAY); if (updateRequired) { updateRequired = false; - xSemaphoreTake(renderingMutex, portMAX_DELAY); render(); - xSemaphoreGive(renderingMutex); } + xSemaphoreGive(renderingMutex); vTaskDelay(10 / portTICK_PERIOD_MS); } } @@ -179,8 +190,11 @@ void FileSelectionActivity::render() const { const auto pageWidth = renderer.getScreenWidth(); - // Draw header - renderer.drawCenteredText(UI_12_FONT_ID, headerY, "Books", true, EpdFontFamily::BOLD); + // Draw header with path + 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 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); if (files.empty()) { - renderer.drawText(UI_10_FONT_ID, horizontalMargin + 4, listStartY, "No books found"); - renderer.displayBuffer(); + const int emptyY = listStartY + 40; + 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; } @@ -203,5 +225,11 @@ void FileSelectionActivity::render() const { 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(); + } } diff --git a/src/activities/reader/FileSelectionActivity.h b/src/activities/reader/FileSelectionActivity.h index 88e97d0c..3678b240 100644 --- a/src/activities/reader/FileSelectionActivity.h +++ b/src/activities/reader/FileSelectionActivity.h @@ -16,6 +16,7 @@ class FileSelectionActivity final : public Activity { std::vector files; int selectorIndex = 0; bool updateRequired = false; + mutable std::string lastRenderedPath; const std::function onSelect; const std::function onGoHome; diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index fd732924..2423d473 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -43,6 +43,7 @@ void XtcReaderChapterSelectionActivity::taskTrampoline(void* param) { void XtcReaderChapterSelectionActivity::onEnter() { Activity::onEnter(); + isFirstRender = true; if (!xtc) { return; @@ -130,22 +131,49 @@ void XtcReaderChapterSelectionActivity::renderScreen() { const auto pageWidth = renderer.getScreenWidth(); 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(); 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(); return; } + // Draw selection highlight 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(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); + 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(); + } } diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.h b/src/activities/reader/XtcReaderChapterSelectionActivity.h index f0fe06bb..5f823e1a 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.h +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.h @@ -15,6 +15,7 @@ class XtcReaderChapterSelectionActivity final : public Activity { uint32_t currentPage = 0; int selectorIndex = 0; bool updateRequired = false; + mutable bool isFirstRender = true; const std::function onGoBack; const std::function onSelectPage; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 56227f97..f6a48768 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -53,6 +53,7 @@ void SettingsActivity::taskTrampoline(void* param) { void SettingsActivity::onEnter() { Activity::onEnter(); + isFirstRender = true; renderingMutex = xSemaphoreCreateMutex(); @@ -215,6 +216,11 @@ void SettingsActivity::render() const { const auto labels = mappedInput.mapLabels("« Save", "Toggle", "", ""); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - // Always use standard refresh for settings screen - renderer.displayBuffer(); + // Use HALF_REFRESH on first render to clear ghosting, then FAST_REFRESH + if (isFirstRender) { + renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + isFirstRender = false; + } else { + renderer.displayBuffer(); + } } diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 83beb9d9..4479a6d6 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -25,6 +25,7 @@ class SettingsActivity final : public ActivityWithSubactivity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; bool updateRequired = false; + mutable bool isFirstRender = true; int selectedSettingIndex = 0; // Currently selected setting const std::function onGoHome;