This commit is contained in:
Jesse Vincent 2026-02-04 09:18:12 +11:00 committed by GitHub
commit 54ef16928c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 36 additions and 24 deletions

View File

@ -3,8 +3,8 @@
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <Serialization.h> #include <Serialization.h>
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int 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); block->render(renderer, fontId, xPos + xOffset, yPos + yOffset, black);
} }
bool PageLine::serialize(FsFile& file) { bool PageLine::serialize(FsFile& file) {
@ -25,9 +25,10 @@ std::unique_ptr<PageLine> PageLine::deserialize(FsFile& file) {
return std::unique_ptr<PageLine>(new PageLine(std::move(tb), xPos, yPos)); return std::unique_ptr<PageLine>(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) { for (auto& element : elements) {
element->render(renderer, fontId, xOffset, yOffset); element->render(renderer, fontId, xOffset, yOffset, black);
} }
} }

View File

@ -17,7 +17,7 @@ class PageElement {
int16_t yPos; int16_t yPos;
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {} explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
virtual ~PageElement() = default; 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; virtual bool serialize(FsFile& file) = 0;
}; };
@ -28,7 +28,7 @@ class PageLine final : public PageElement {
public: public:
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos) PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
: PageElement(xPos, yPos), block(std::move(block)) {} : 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; bool serialize(FsFile& file) override;
static std::unique_ptr<PageLine> deserialize(FsFile& file); static std::unique_ptr<PageLine> deserialize(FsFile& file);
}; };
@ -37,7 +37,7 @@ class Page {
public: public:
// the list of block index and line numbers on this page // the list of block index and line numbers on this page
std::vector<std::shared_ptr<PageElement>> elements; std::vector<std::shared_ptr<PageElement>> 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; bool serialize(FsFile& file) const;
static std::unique_ptr<Page> deserialize(FsFile& file); static std::unique_ptr<Page> deserialize(FsFile& file);
}; };

View File

@ -3,7 +3,8 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <Serialization.h> #include <Serialization.h>
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 // Validate iterator bounds before rendering
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) { 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(), 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(); auto wordXposIt = wordXpos.begin();
for (size_t i = 0; i < words.size(); i++) { 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(wordIt, 1);
std::advance(wordStylesIt, 1); std::advance(wordStylesIt, 1);

View File

@ -34,7 +34,7 @@ class TextBlock final : public Block {
bool isEmpty() override { return words.empty(); } bool isEmpty() override { return words.empty(); }
void layout(GfxRenderer& renderer) override {}; void layout(GfxRenderer& renderer) override {};
// given a renderer works out where to break the words into lines // 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; } BlockType getType() override { return TEXT_BLOCK; }
bool serialize(FsFile& file) const; bool serialize(FsFile& file) const;
static std::unique_ptr<TextBlock> deserialize(FsFile& file); static std::unique_ptr<TextBlock> deserialize(FsFile& file);

View File

@ -638,7 +638,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
drawPixel(screenX, screenY, black); drawPixel(screenX, screenY, black);
} else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) { } else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
drawPixel(screenX, screenY, false); drawPixel(screenX, screenY, false);
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) { } else if (renderMode == GRAYSCALE_LSB && bmpVal == (black ? 1 : 2)) {
drawPixel(screenX, screenY, false); drawPixel(screenX, screenY, false);
} }
} else { } 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) // 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 // We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update
drawPixel(screenX, screenY, false); drawPixel(screenX, screenY, false);
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) { } else if (renderMode == GRAYSCALE_LSB && bmpVal == (pixelState ? 1 : 2)) {
// Dark gray // Dark gray (swap gray level for inverse display)
drawPixel(screenX, screenY, false); drawPixel(screenX, screenY, false);
} }
} else { } else {

View File

@ -22,7 +22,7 @@ void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) {
namespace { namespace {
constexpr uint8_t SETTINGS_FILE_VERSION = 1; constexpr uint8_t SETTINGS_FILE_VERSION = 1;
// Increment this when adding new persisted settings fields // 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"; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin";
} // namespace } // namespace
@ -60,6 +60,7 @@ bool CrossPointSettings::saveToFile() const {
serialization::writeString(outputFile, std::string(opdsUsername)); serialization::writeString(outputFile, std::string(opdsUsername));
serialization::writeString(outputFile, std::string(opdsPassword)); serialization::writeString(outputFile, std::string(opdsPassword));
serialization::writePod(outputFile, sleepScreenCoverFilter); serialization::writePod(outputFile, sleepScreenCoverFilter);
serialization::writePod(outputFile, inverseDisplay);
// New fields added at end for backward compatibility // New fields added at end for backward compatibility
outputFile.close(); outputFile.close();
@ -148,6 +149,8 @@ bool CrossPointSettings::loadFromFile() {
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT); readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT);
if (++settingsRead >= fileSettingsCount) break; if (++settingsRead >= fileSettingsCount) break;
serialization::readPod(inputFile, inverseDisplay);
if (++settingsRead >= fileSettingsCount) break;
// New fields added at end for backward compatibility // New fields added at end for backward compatibility
} while (false); } while (false);

View File

@ -137,6 +137,8 @@ class CrossPointSettings {
uint8_t hideBatteryPercentage = HIDE_NEVER; uint8_t hideBatteryPercentage = HIDE_NEVER;
// Long-press chapter skip on side buttons // Long-press chapter skip on side buttons
uint8_t longPressChapterSkip = 1; uint8_t longPressChapterSkip = 1;
// Inverse display (white text on black background)
uint8_t inverseDisplay = 0;
~CrossPointSettings() = default; ~CrossPointSettings() = default;

View File

@ -400,11 +400,12 @@ void EpubReaderActivity::renderScreen() {
} }
} }
renderer.clearScreen(); const bool inverse = SETTINGS.inverseDisplay;
renderer.clearScreen(inverse ? 0x00 : 0xFF);
if (section->pageCount == 0) { if (section->pageCount == 0) {
Serial.printf("[%lu] [ERS] No pages to render\n", millis()); 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); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
@ -412,7 +413,7 @@ void EpubReaderActivity::renderScreen() {
if (section->currentPage < 0 || section->currentPage >= section->pageCount) { 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); 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); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
@ -453,7 +454,8 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop, void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginRight, const int orientedMarginBottom,
const int orientedMarginLeft) { 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); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
if (pagesUntilFullRefresh <= 1) { if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(HalDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
@ -471,13 +473,13 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
if (SETTINGS.textAntiAliasing) { if (SETTINGS.textAntiAliasing) {
renderer.clearScreen(0x00); renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop, !inverse);
renderer.copyGrayscaleLsbBuffers(); renderer.copyGrayscaleLsbBuffers();
// Render and copy to MSB buffer // Render and copy to MSB buffer
renderer.clearScreen(0x00); renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop, !inverse);
renderer.copyGrayscaleMsbBuffers(); renderer.copyGrayscaleMsbBuffers();
// display grayscale part // display grayscale part

View File

@ -393,7 +393,7 @@ void TxtReaderActivity::renderScreen() {
currentPageLines.clear(); currentPageLines.clear();
loadPageAtOffset(offset, currentPageLines, nextOffset); loadPageAtOffset(offset, currentPageLines, nextOffset);
renderer.clearScreen(); renderer.clearScreen(SETTINGS.inverseDisplay ? 0x00 : 0xFF);
renderPage(); renderPage();
// Save progress // Save progress
@ -412,6 +412,8 @@ void TxtReaderActivity::renderPage() {
const int lineHeight = renderer.getLineHeight(cachedFontId); const int lineHeight = renderer.getLineHeight(cachedFontId);
const int contentWidth = viewportWidth; const int contentWidth = viewportWidth;
const bool inverse = SETTINGS.inverseDisplay;
// Render text lines with alignment // Render text lines with alignment
auto renderLines = [&]() { auto renderLines = [&]() {
int y = orientedMarginTop; int y = orientedMarginTop;
@ -441,7 +443,7 @@ void TxtReaderActivity::renderPage() {
break; break;
} }
renderer.drawText(cachedFontId, x, y, line.c_str()); renderer.drawText(cachedFontId, x, y, line.c_str(), !inverse);
} }
y += lineHeight; y += lineHeight;
} }

View File

@ -24,7 +24,7 @@ const SettingInfo displaySettings[displaySettingsCount] = {
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})}; {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})};
constexpr int readerSettingsCount = 9; constexpr int readerSettingsCount = 10;
const SettingInfo readerSettings[readerSettingsCount] = { const SettingInfo readerSettings[readerSettingsCount] = {
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}), SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}),
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}), 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, SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation,
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}), {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}),
SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing), 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; constexpr int controlsSettingsCount = 4;
const SettingInfo controlsSettings[controlsSettingsCount] = { const SettingInfo controlsSettings[controlsSettingsCount] = {