Compare commits

...

3 Commits

Author SHA1 Message Date
Jesse Vincent
c509eb9893
Merge aa2467de9e into d403044f76 2026-02-03 17:56:33 +01:00
Aaron Cunliffe
d403044f76
fix: Increase network SSID display length (#670)
Some checks are pending
CI / build (push) Waiting to run
## Rationale 

I have 2 wifi access points with almost identical names, just one has
`_EXT` at the end of it. With the current display limit of 13 characters
before adding ellipsis, I can't tell which is which.

Before device screenshot with masked SSIDs:
<img
src="https://github.com/user-attachments/assets/3c5cbbaa-b2f6-412f-b5a8-6278963bd0f2"
width="300">


## Summary

Adjusted displayed length from 13 characters to 30 in the Wifi selection
screen - I've left some space for potential proportional font changes in
the future

After image with masked SSIDs:
<img
src="https://github.com/user-attachments/assets/c5f0712b-bbd3-4eec-9820-4693fae90c9f"
width="300">

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< NO >**_
2026-02-03 18:24:23 +03:00
Jesse Vincent
aa2467de9e feat: Add inverse display setting with proper anti-aliasing
Adds an inverseDisplay setting that renders white text on a black
background in both EPUB and TXT readers. The grayscale anti-aliasing
passes swap the LSB gray level condition based on text direction
(pixelState/black parameter) so intermediate gray values render
correctly against the dark background.
2026-02-01 23:46:57 -08:00
11 changed files with 38 additions and 26 deletions

View File

@ -3,8 +3,8 @@
#include <HardwareSerial.h>
#include <Serialization.h>
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> PageLine::deserialize(FsFile& file) {
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) {
element->render(renderer, fontId, xOffset, yOffset);
element->render(renderer, fontId, xOffset, yOffset, black);
}
}

View File

@ -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<TextBlock> 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<PageLine> deserialize(FsFile& file);
};
@ -37,7 +37,7 @@ class Page {
public:
// the list of block index and line numbers on this page
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;
static std::unique_ptr<Page> deserialize(FsFile& file);
};

View File

@ -3,7 +3,8 @@
#include <GfxRenderer.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
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);

View File

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

View File

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

View File

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

View File

@ -546,8 +546,8 @@ void WifiSelectionActivity::renderNetworkList() const {
// Draw network name (truncate if too long)
std::string displayName = network.ssid;
if (displayName.length() > 16) {
displayName.replace(13, displayName.length() - 13, "...");
if (displayName.length() > 33) {
displayName.replace(30, displayName.length() - 30, "...");
}
renderer.drawText(UI_10_FONT_ID, 20, networkY, displayName.c_str());

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) {
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> 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> 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

View File

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

View File

@ -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] = {