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:
ratedcounsel 2025-12-31 13:35:46 +00:00
parent 73959ea9ac
commit 768f2bd815
10 changed files with 104 additions and 19 deletions

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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<void()> onGoBack;
const std::function<void(int newSpineIndex)> onSelectSpineIndex;

View File

@ -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();
}
}

View File

@ -16,6 +16,7 @@ class FileSelectionActivity final : public Activity {
std::vector<std::string> files;
int selectorIndex = 0;
bool updateRequired = false;
mutable std::string lastRenderedPath;
const std::function<void(const std::string&)> onSelect;
const std::function<void()> onGoHome;

View File

@ -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<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);
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();
}
}

View File

@ -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<void()> onGoBack;
const std::function<void(uint32_t newPage)> onSelectPage;

View File

@ -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();
}
}

View File

@ -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<void()> onGoHome;