mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-05 07:07: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() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user