diff --git a/lib/Epub/Epub/Page.cpp b/lib/Epub/Epub/Page.cpp index 92839eb7..aa09d2c6 100644 --- a/lib/Epub/Epub/Page.cpp +++ b/lib/Epub/Epub/Page.cpp @@ -3,8 +3,8 @@ #include #include -void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) { - block->render(renderer, fontId, xPos + xOffset, yPos + yOffset); +void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset, const bool black) { + block->render(renderer, fontId, xPos + xOffset, yPos + yOffset, black); } bool PageLine::serialize(FsFile& file) { @@ -25,9 +25,10 @@ std::unique_ptr PageLine::deserialize(FsFile& file) { return std::unique_ptr(new PageLine(std::move(tb), xPos, yPos)); } -void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const { +void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset, + const bool black) const { for (auto& element : elements) { - element->render(renderer, fontId, xOffset, yOffset); + element->render(renderer, fontId, xOffset, yOffset, black); } } diff --git a/lib/Epub/Epub/Page.h b/lib/Epub/Epub/Page.h index 20061941..9662b2d7 100644 --- a/lib/Epub/Epub/Page.h +++ b/lib/Epub/Epub/Page.h @@ -17,7 +17,7 @@ class PageElement { int16_t yPos; explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {} virtual ~PageElement() = default; - virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0; + virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool black = true) = 0; virtual bool serialize(FsFile& file) = 0; }; @@ -28,7 +28,7 @@ class PageLine final : public PageElement { public: PageLine(std::shared_ptr block, const int16_t xPos, const int16_t yPos) : PageElement(xPos, yPos), block(std::move(block)) {} - void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override; + void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool black = true) override; bool serialize(FsFile& file) override; static std::unique_ptr deserialize(FsFile& file); }; @@ -37,7 +37,7 @@ class Page { public: // the list of block index and line numbers on this page std::vector> elements; - void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const; + void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool black = true) const; bool serialize(FsFile& file) const; static std::unique_ptr deserialize(FsFile& file); }; diff --git a/lib/Epub/Epub/blocks/TextBlock.cpp b/lib/Epub/Epub/blocks/TextBlock.cpp index 2a15aef0..7d4a300f 100644 --- a/lib/Epub/Epub/blocks/TextBlock.cpp +++ b/lib/Epub/Epub/blocks/TextBlock.cpp @@ -3,7 +3,8 @@ #include #include -void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const { +void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y, + const bool black) const { // Validate iterator bounds before rendering if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) { Serial.printf("[%lu] [TXB] Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(), @@ -16,7 +17,7 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int auto wordXposIt = wordXpos.begin(); for (size_t i = 0; i < words.size(); i++) { - renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), true, *wordStylesIt); + renderer.drawText(fontId, *wordXposIt + x, y, wordIt->c_str(), black, *wordStylesIt); std::advance(wordIt, 1); std::advance(wordStylesIt, 1); diff --git a/lib/Epub/Epub/blocks/TextBlock.h b/lib/Epub/Epub/blocks/TextBlock.h index 415a18f3..e0185688 100644 --- a/lib/Epub/Epub/blocks/TextBlock.h +++ b/lib/Epub/Epub/blocks/TextBlock.h @@ -34,7 +34,7 @@ class TextBlock final : public Block { bool isEmpty() override { return words.empty(); } void layout(GfxRenderer& renderer) override {}; // given a renderer works out where to break the words into lines - void render(const GfxRenderer& renderer, int fontId, int x, int y) const; + void render(const GfxRenderer& renderer, int fontId, int x, int y, bool black = true) const; BlockType getType() override { return TEXT_BLOCK; } bool serialize(FsFile& file) const; static std::unique_ptr deserialize(FsFile& file); diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index b5aa7710..387002f1 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -638,7 +638,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y drawPixel(screenX, screenY, black); } else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) { drawPixel(screenX, screenY, false); - } else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) { + } else if (renderMode == GRAYSCALE_LSB && bmpVal == (black ? 1 : 2)) { drawPixel(screenX, screenY, false); } } else { @@ -822,8 +822,8 @@ void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp, // Light gray (also mark the MSB if it's going to be a dark gray too) // We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update drawPixel(screenX, screenY, false); - } else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) { - // Dark gray + } else if (renderMode == GRAYSCALE_LSB && bmpVal == (pixelState ? 1 : 2)) { + // Dark gray (swap gray level for inverse display) drawPixel(screenX, screenY, false); } } else { diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 232c7c57..badcd143 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -22,7 +22,7 @@ void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) { namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 23; +constexpr uint8_t SETTINGS_COUNT = 24; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace @@ -60,6 +60,7 @@ bool CrossPointSettings::saveToFile() const { serialization::writeString(outputFile, std::string(opdsUsername)); serialization::writeString(outputFile, std::string(opdsPassword)); serialization::writePod(outputFile, sleepScreenCoverFilter); + serialization::writePod(outputFile, inverseDisplay); // New fields added at end for backward compatibility outputFile.close(); @@ -148,6 +149,8 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT); if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, inverseDisplay); + if (++settingsRead >= fileSettingsCount) break; // New fields added at end for backward compatibility } while (false); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index c450d348..4674d2df 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -137,6 +137,8 @@ class CrossPointSettings { uint8_t hideBatteryPercentage = HIDE_NEVER; // Long-press chapter skip on side buttons uint8_t longPressChapterSkip = 1; + // Inverse display (white text on black background) + uint8_t inverseDisplay = 0; ~CrossPointSettings() = default; diff --git a/src/ScreenComponents.cpp b/src/ScreenComponents.cpp index 72f7faf0..8dd6a5ba 100644 --- a/src/ScreenComponents.cpp +++ b/src/ScreenComponents.cpp @@ -9,11 +9,11 @@ #include "fontIds.h" void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left, const int top, - const bool showPercentage) { + const bool showPercentage, const bool black) { // Left aligned battery icon and percentage const uint16_t percentage = battery.readPercentage(); const auto percentageText = showPercentage ? std::to_string(percentage) + "%" : ""; - renderer.drawText(SMALL_FONT_ID, left + 20, top, percentageText.c_str()); + renderer.drawText(SMALL_FONT_ID, left + 20, top, percentageText.c_str(), black); // 1 column on left, 2 columns on right, 5 columns of battery body constexpr int batteryWidth = 15; @@ -22,16 +22,16 @@ void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left, const int y = top + 6; // Top line - renderer.drawLine(x + 1, y, x + batteryWidth - 3, y); + renderer.drawLine(x + 1, y, x + batteryWidth - 3, y, black); // Bottom line - renderer.drawLine(x + 1, y + batteryHeight - 1, x + batteryWidth - 3, y + batteryHeight - 1); + renderer.drawLine(x + 1, y + batteryHeight - 1, x + batteryWidth - 3, y + batteryHeight - 1, black); // Left line - renderer.drawLine(x, y + 1, x, y + batteryHeight - 2); + renderer.drawLine(x, y + 1, x, y + batteryHeight - 2, black); // Battery end - renderer.drawLine(x + batteryWidth - 2, y + 1, x + batteryWidth - 2, y + batteryHeight - 2); - renderer.drawPixel(x + batteryWidth - 1, y + 3); - renderer.drawPixel(x + batteryWidth - 1, y + batteryHeight - 4); - renderer.drawLine(x + batteryWidth - 0, y + 4, x + batteryWidth - 0, y + batteryHeight - 5); + renderer.drawLine(x + batteryWidth - 2, y + 1, x + batteryWidth - 2, y + batteryHeight - 2, black); + renderer.drawPixel(x + batteryWidth - 1, y + 3, black); + renderer.drawPixel(x + batteryWidth - 1, y + batteryHeight - 4, black); + renderer.drawLine(x + batteryWidth - 0, y + 4, x + batteryWidth - 0, y + batteryHeight - 5, black); // The +1 is to round up, so that we always fill at least one pixel int filledWidth = percentage * (batteryWidth - 5) / 100 + 1; @@ -39,7 +39,7 @@ void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left, filledWidth = batteryWidth - 5; // Ensure we don't overflow } - renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4); + renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4, black); } ScreenComponents::PopupLayout ScreenComponents::drawPopup(const GfxRenderer& renderer, const char* message) { @@ -74,7 +74,7 @@ void ScreenComponents::fillPopupProgress(const GfxRenderer& renderer, const Popu renderer.displayBuffer(HalDisplay::FAST_REFRESH); } -void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const size_t bookProgress) { +void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const size_t bookProgress, const bool black) { int vieweableMarginTop, vieweableMarginRight, vieweableMarginBottom, vieweableMarginLeft; renderer.getOrientedViewableTRBL(&vieweableMarginTop, &vieweableMarginRight, &vieweableMarginBottom, &vieweableMarginLeft); @@ -82,7 +82,7 @@ void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const si const int progressBarMaxWidth = renderer.getScreenWidth() - vieweableMarginLeft - vieweableMarginRight; const int progressBarY = renderer.getScreenHeight() - vieweableMarginBottom - BOOK_PROGRESS_BAR_HEIGHT; const int barWidth = progressBarMaxWidth * bookProgress / 100; - renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, true); + renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, black); } int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector& tabs) { diff --git a/src/ScreenComponents.h b/src/ScreenComponents.h index 78ed5920..9ecd2494 100644 --- a/src/ScreenComponents.h +++ b/src/ScreenComponents.h @@ -22,8 +22,9 @@ class ScreenComponents { int height; }; - static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true); - static void drawBookProgressBar(const GfxRenderer& renderer, size_t bookProgress); + static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true, + bool black = true); + static void drawBookProgressBar(const GfxRenderer& renderer, size_t bookProgress, bool black = true); static PopupLayout drawPopup(const GfxRenderer& renderer, const char* message); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 5ccfb4fe..336f9388 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -400,11 +400,12 @@ void EpubReaderActivity::renderScreen() { } } - renderer.clearScreen(); + const bool inverse = SETTINGS.inverseDisplay; + renderer.clearScreen(inverse ? 0x00 : 0xFF); if (section->pageCount == 0) { Serial.printf("[%lu] [ERS] No pages to render\n", millis()); - renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", !inverse, EpdFontFamily::BOLD); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderer.displayBuffer(); return; @@ -412,7 +413,7 @@ void EpubReaderActivity::renderScreen() { if (section->currentPage < 0 || section->currentPage >= section->pageCount) { Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage, section->pageCount); - renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", !inverse, EpdFontFamily::BOLD); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderer.displayBuffer(); return; @@ -453,7 +454,8 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC void EpubReaderActivity::renderContents(std::unique_ptr page, const int orientedMarginTop, const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginLeft) { - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + const bool inverse = SETTINGS.inverseDisplay; + page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop, !inverse); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); if (pagesUntilFullRefresh <= 1) { renderer.displayBuffer(HalDisplay::HALF_REFRESH); @@ -471,13 +473,13 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or if (SETTINGS.textAntiAliasing) { renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop, !inverse); renderer.copyGrayscaleLsbBuffers(); // Render and copy to MSB buffer renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); - page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); + page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop, !inverse); renderer.copyGrayscaleMsbBuffers(); // display grayscale part @@ -491,6 +493,8 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginLeft) const { + const bool inverse = SETTINGS.inverseDisplay; + // determine visible status bar elements const bool showProgressPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || @@ -529,16 +533,16 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr); renderer.drawText(SMALL_FONT_ID, renderer.getScreenWidth() - orientedMarginRight - progressTextWidth, textY, - progressStr); + progressStr, !inverse); } if (showProgressBar) { // Draw progress bar at the very bottom of the screen, from edge to edge of viewable area - ScreenComponents::drawBookProgressBar(renderer, static_cast(bookProgress)); + ScreenComponents::drawBookProgressBar(renderer, static_cast(bookProgress), !inverse); } if (showBattery) { - ScreenComponents::drawBattery(renderer, orientedMarginLeft + 1, textY, showBatteryPercentage); + ScreenComponents::drawBattery(renderer, orientedMarginLeft + 1, textY, showBatteryPercentage, !inverse); } if (showChapterTitle) { @@ -578,6 +582,6 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in renderer.drawText(SMALL_FONT_ID, titleMarginLeftAdjusted + orientedMarginLeft + (availableTitleSpace - titleWidth) / 2, textY, - title.c_str()); + title.c_str(), !inverse); } } diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index eb1a9eef..d463b51a 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -393,7 +393,7 @@ void TxtReaderActivity::renderScreen() { currentPageLines.clear(); loadPageAtOffset(offset, currentPageLines, nextOffset); - renderer.clearScreen(); + renderer.clearScreen(SETTINGS.inverseDisplay ? 0x00 : 0xFF); renderPage(); // Save progress @@ -412,6 +412,8 @@ void TxtReaderActivity::renderPage() { const int lineHeight = renderer.getLineHeight(cachedFontId); const int contentWidth = viewportWidth; + const bool inverse = SETTINGS.inverseDisplay; + // Render text lines with alignment auto renderLines = [&]() { int y = orientedMarginTop; @@ -441,7 +443,7 @@ void TxtReaderActivity::renderPage() { break; } - renderer.drawText(cachedFontId, x, y, line.c_str()); + renderer.drawText(cachedFontId, x, y, line.c_str(), !inverse); } y += lineHeight; } @@ -484,6 +486,8 @@ void TxtReaderActivity::renderPage() { void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginLeft) const { + const bool inverse = SETTINGS.inverseDisplay; + const bool showProgressPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR; @@ -514,16 +518,16 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr); renderer.drawText(SMALL_FONT_ID, renderer.getScreenWidth() - orientedMarginRight - progressTextWidth, textY, - progressStr); + progressStr, !inverse); } if (showProgressBar) { // Draw progress bar at the very bottom of the screen, from edge to edge of viewable area - ScreenComponents::drawBookProgressBar(renderer, static_cast(progress)); + ScreenComponents::drawBookProgressBar(renderer, static_cast(progress), !inverse); } if (showBattery) { - ScreenComponents::drawBattery(renderer, orientedMarginLeft, textY, showBatteryPercentage); + ScreenComponents::drawBattery(renderer, orientedMarginLeft, textY, showBatteryPercentage, !inverse); } if (showTitle) { @@ -538,7 +542,8 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); } - renderer.drawText(SMALL_FONT_ID, titleMarginLeft + (availableTextWidth - titleWidth) / 2, textY, title.c_str()); + renderer.drawText(SMALL_FONT_ID, titleMarginLeft + (availableTextWidth - titleWidth) / 2, textY, title.c_str(), + !inverse); } } diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 7316db05..4c03fa38 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -24,7 +24,7 @@ const SettingInfo displaySettings[displaySettingsCount] = { SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})}; -constexpr int readerSettingsCount = 9; +constexpr int readerSettingsCount = 10; const SettingInfo readerSettings[readerSettingsCount] = { SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}), SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}), @@ -36,7 +36,8 @@ const SettingInfo readerSettings[readerSettingsCount] = { SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation, {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}), SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing), - SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)}; + SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing), + SettingInfo::Toggle("Inverse Display", &CrossPointSettings::inverseDisplay)}; constexpr int controlsSettingsCount = 4; const SettingInfo controlsSettings[controlsSettingsCount] = {