From 5e9626eb2ad7a545fb0ec2f99e65a8276d15a9f9 Mon Sep 17 00:00:00 2001 From: Maeve Andrews <37351465+maeveynot@users.noreply.github.com> Date: Fri, 2 Jan 2026 01:21:48 -0600 Subject: [PATCH] Add paragraph alignment setting (justify/left/center/right) (#191) ## Summary * **What is the goal of this PR?** Add a new user setting for paragraph alignment, instead of hard-coding full justification. * **What changes are included?** One new line in the settings screen, with 4 options (justify/left/center/right). Default is justified since that's what it was already. I personally only wanted to disable justification and use "left", but I included the other options for completeness since they were already supported. ## Additional Context Tested on my X4 and looks as expected for each alignment. Co-authored-by: Maeve Andrews --- lib/Epub/Epub/Section.cpp | 33 +++++++++++-------- lib/Epub/Epub/Section.h | 13 ++++---- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 4 +-- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h | 5 ++- src/CrossPointSettings.cpp | 5 ++- src/CrossPointSettings.h | 2 ++ src/activities/reader/EpubReaderActivity.cpp | 7 ++-- src/activities/settings/SettingsActivity.cpp | 6 +++- 8 files changed, 48 insertions(+), 27 deletions(-) diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 1f99f018..18b81aae 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -7,9 +7,9 @@ #include "parsers/ChapterHtmlSlimParser.h" namespace { -constexpr uint8_t SECTION_FILE_VERSION = 8; -constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint16_t) + - sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t); +constexpr uint8_t SECTION_FILE_VERSION = 9; +constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) + + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint32_t); } // namespace uint32_t Section::onPageComplete(std::unique_ptr page) { @@ -30,19 +30,21 @@ uint32_t Section::onPageComplete(std::unique_ptr page) { } void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing, - const uint16_t viewportWidth, const uint16_t viewportHeight) { + const uint8_t paragraphAlignment, const uint16_t viewportWidth, + const uint16_t viewportHeight) { if (!file) { Serial.printf("[%lu] [SCT] File not open for writing header\n", millis()); return; } static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) + - sizeof(extraParagraphSpacing) + sizeof(viewportWidth) + sizeof(viewportHeight) + - sizeof(pageCount) + sizeof(uint32_t), + sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) + + sizeof(viewportHeight) + sizeof(pageCount) + sizeof(uint32_t), "Header size mismatch"); serialization::writePod(file, SECTION_FILE_VERSION); serialization::writePod(file, fontId); serialization::writePod(file, lineCompression); serialization::writePod(file, extraParagraphSpacing); + serialization::writePod(file, paragraphAlignment); serialization::writePod(file, viewportWidth); serialization::writePod(file, viewportHeight); serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written) @@ -50,7 +52,8 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi } bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, - const uint16_t viewportWidth, const uint16_t viewportHeight) { + const uint8_t paragraphAlignment, const uint16_t viewportWidth, + const uint16_t viewportHeight) { if (!SdMan.openFileForRead("SCT", filePath, file)) { return false; } @@ -70,15 +73,17 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con uint16_t fileViewportWidth, fileViewportHeight; float fileLineCompression; bool fileExtraParagraphSpacing; + uint8_t fileParagraphAlignment; serialization::readPod(file, fileFontId); serialization::readPod(file, fileLineCompression); serialization::readPod(file, fileExtraParagraphSpacing); + serialization::readPod(file, fileParagraphAlignment); serialization::readPod(file, fileViewportWidth); serialization::readPod(file, fileViewportHeight); if (fontId != fileFontId || lineCompression != fileLineCompression || - extraParagraphSpacing != fileExtraParagraphSpacing || viewportWidth != fileViewportWidth || - viewportHeight != fileViewportHeight) { + extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment || + viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight) { file.close(); Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis()); clearCache(); @@ -109,8 +114,8 @@ bool Section::clearCache() const { } bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, - const uint16_t viewportWidth, const uint16_t viewportHeight, - const std::function& progressSetupFn, + const uint8_t paragraphAlignment, const uint16_t viewportWidth, + const uint16_t viewportHeight, const std::function& progressSetupFn, const std::function& progressFn) { constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB const auto localPath = epub->getSpineItem(spineIndex).href; @@ -166,11 +171,13 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c if (!SdMan.openFileForWrite("SCT", filePath, file)) { return false; } - writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, viewportWidth, viewportHeight); + writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, + viewportHeight); std::vector lut = {}; ChapterHtmlSlimParser visitor( - tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, viewportWidth, viewportHeight, + tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, + viewportHeight, [this, &lut](std::unique_ptr page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, progressFn); success = visitor.parseAndBuildPages(); diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index 55244d0e..bac95efd 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -14,8 +14,8 @@ class Section { std::string filePath; FsFile file; - void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint16_t viewportWidth, - uint16_t viewportHeight); + void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, + uint16_t viewportWidth, uint16_t viewportHeight); uint32_t onPageComplete(std::unique_ptr page); public: @@ -28,11 +28,12 @@ class Section { renderer(renderer), filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {} ~Section() = default; - bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint16_t viewportWidth, - uint16_t viewportHeight); + bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, + uint16_t viewportWidth, uint16_t viewportHeight); bool clearCache() const; - bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint16_t viewportWidth, - uint16_t viewportHeight, const std::function& progressSetupFn = nullptr, + bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, + uint16_t viewportWidth, uint16_t viewportHeight, + const std::function& progressSetupFn = nullptr, const std::function& progressFn = nullptr); std::unique_ptr loadPageFromSectionFile(); }; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index a2ff485c..e5eb4d10 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -97,7 +97,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* if (strcmp(name, "br") == 0) { self->startNewTextBlock(self->currentTextBlock->getStyle()); } else { - self->startNewTextBlock(TextBlock::JUSTIFIED); + self->startNewTextBlock((TextBlock::Style)self->paragraphAlignment); } } else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) { self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth); @@ -221,7 +221,7 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n } bool ChapterHtmlSlimParser::parseAndBuildPages() { - startNewTextBlock(TextBlock::JUSTIFIED); + startNewTextBlock((TextBlock::Style)this->paragraphAlignment); const XML_Parser parser = XML_ParserCreate(nullptr); int done; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 795c2c33..c559e157 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -33,6 +33,7 @@ class ChapterHtmlSlimParser { int fontId; float lineCompression; bool extraParagraphSpacing; + uint8_t paragraphAlignment; uint16_t viewportWidth; uint16_t viewportHeight; @@ -46,7 +47,8 @@ class ChapterHtmlSlimParser { public: explicit ChapterHtmlSlimParser(const std::string& filepath, GfxRenderer& renderer, const int fontId, const float lineCompression, const bool extraParagraphSpacing, - const uint16_t viewportWidth, const uint16_t viewportHeight, + const uint8_t paragraphAlignment, const uint16_t viewportWidth, + const uint16_t viewportHeight, const std::function)>& completePageFn, const std::function& progressFn = nullptr) : filepath(filepath), @@ -54,6 +56,7 @@ class ChapterHtmlSlimParser { fontId(fontId), lineCompression(lineCompression), extraParagraphSpacing(extraParagraphSpacing), + paragraphAlignment(paragraphAlignment), viewportWidth(viewportWidth), viewportHeight(viewportHeight), completePageFn(completePageFn), diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 74a95959..74bc0d26 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -12,7 +12,7 @@ CrossPointSettings CrossPointSettings::instance; namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 10; +constexpr uint8_t SETTINGS_COUNT = 11; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace @@ -37,6 +37,7 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, fontFamily); serialization::writePod(outputFile, fontSize); serialization::writePod(outputFile, lineSpacing); + serialization::writePod(outputFile, paragraphAlignment); outputFile.close(); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); @@ -83,6 +84,8 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, lineSpacing); if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, paragraphAlignment); + if (++settingsRead >= fileSettingsCount) break; } while (false); inputFile.close(); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 108aecdf..b9cce85e 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -43,6 +43,7 @@ class CrossPointSettings { // Font size options enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 }; enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 }; + enum PARAGRAPH_ALIGNMENT { JUSTIFIED = 0, LEFT_ALIGN = 1, CENTER_ALIGN = 2, RIGHT_ALIGN = 3 }; // Sleep screen settings uint8_t sleepScreen = DARK; @@ -62,6 +63,7 @@ class CrossPointSettings { uint8_t fontFamily = BOOKERLY; uint8_t fontSize = MEDIUM; uint8_t lineSpacing = NORMAL; + uint8_t paragraphAlignment = JUSTIFIED; ~CrossPointSettings() = default; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index fae5d241..4348625d 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -267,7 +267,8 @@ void EpubReaderActivity::renderScreen() { const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom; if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), - SETTINGS.extraParagraphSpacing, viewportWidth, viewportHeight)) { + SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, + viewportHeight)) { Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); // Progress bar dimensions @@ -311,8 +312,8 @@ void EpubReaderActivity::renderScreen() { }; if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), - SETTINGS.extraParagraphSpacing, viewportWidth, viewportHeight, progressSetup, - progressCallback)) { + SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, + viewportHeight, progressSetup, progressCallback)) { Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis()); section.reset(); return; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index fa0cc084..d9d19411 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -9,7 +9,7 @@ // Define the static settings list namespace { -constexpr int settingsCount = 11; +constexpr int settingsCount = 12; const SettingInfo settingsList[settingsCount] = { // Should match with SLEEP_SCREEN_MODE {"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}}, @@ -34,6 +34,10 @@ const SettingInfo settingsList[settingsCount] = { {"Bookerly", "Noto Sans", "Open Dyslexic"}}, {"Reader Font Size", SettingType::ENUM, &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}}, {"Reader Line Spacing", SettingType::ENUM, &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}}, + {"Reader Paragraph Alignment", + SettingType::ENUM, + &CrossPointSettings::paragraphAlignment, + {"Justify", "Left", "Center", "Right"}}, {"Check for updates", SettingType::ACTION, nullptr, {}}, }; } // namespace