diff --git a/lib/Epub/Epub/blocks/TextBlock.h b/lib/Epub/Epub/blocks/TextBlock.h index 85fdd55a..b528ba03 100644 --- a/lib/Epub/Epub/blocks/TextBlock.h +++ b/lib/Epub/Epub/blocks/TextBlock.h @@ -28,6 +28,7 @@ class TextBlock final : public Block { void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; } const BlockStyle& getBlockStyle() const { return blockStyle; } const std::vector& getWords() const { return words; } + const std::vector& getWordXpos() const { return wordXpos; } bool isEmpty() override { return words.empty(); } size_t wordCount() const { return words.size(); } // given a renderer works out where to break the words into lines diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 5876e470..8e269eed 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -61,7 +61,7 @@ static inline void rotateCoordinates(const GfxRenderer::Orientation orientation, } } -enum class TextRotation { None, Rotated90CW }; +enum class TextRotation { None, Rotated90CW, Rotated90CCW }; // Shared glyph rendering logic for normal and rotated text. // Coordinate mapping and cursor advance direction are selected at compile time via the template parameter. @@ -91,6 +91,9 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode if constexpr (rotation == TextRotation::Rotated90CW) { outerBase = cursorX + fontData->ascender - top; // screenX = outerBase + glyphY innerBase = cursorY - left; // screenY = innerBase - glyphX + } else if constexpr (rotation == TextRotation::Rotated90CCW) { + outerBase = cursorX + fontData->advanceY - 1 - fontData->ascender + top; // screenX = outerBase - glyphY + innerBase = cursorY + left; // screenY = innerBase + glyphX } else { outerBase = cursorY - top; // screenY = outerBase + glyphY innerBase = cursorX + left; // screenX = innerBase + glyphX @@ -99,12 +102,16 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode if (is2Bit) { int pixelPosition = 0; for (int glyphY = 0; glyphY < height; glyphY++) { - const int outerCoord = outerBase + glyphY; + const int outerCoord = + (rotation == TextRotation::Rotated90CCW) ? outerBase - glyphY : outerBase + glyphY; for (int glyphX = 0; glyphX < width; glyphX++, pixelPosition++) { int screenX, screenY; if constexpr (rotation == TextRotation::Rotated90CW) { screenX = outerCoord; screenY = innerBase - glyphX; + } else if constexpr (rotation == TextRotation::Rotated90CCW) { + screenX = outerCoord; + screenY = innerBase + glyphX; } else { screenX = innerBase + glyphX; screenY = outerCoord; @@ -133,12 +140,16 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode } else { int pixelPosition = 0; for (int glyphY = 0; glyphY < height; glyphY++) { - const int outerCoord = outerBase + glyphY; + const int outerCoord = + (rotation == TextRotation::Rotated90CCW) ? outerBase - glyphY : outerBase + glyphY; for (int glyphX = 0; glyphX < width; glyphX++, pixelPosition++) { int screenX, screenY; if constexpr (rotation == TextRotation::Rotated90CW) { screenX = outerCoord; screenY = innerBase - glyphX; + } else if constexpr (rotation == TextRotation::Rotated90CCW) { + screenX = outerCoord; + screenY = innerBase + glyphX; } else { screenX = innerBase + glyphX; screenY = outerCoord; @@ -1243,6 +1254,64 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y } } +void GfxRenderer::drawTextRotated90CCW(const int fontId, const int x, const int y, const char* text, const bool black, + const EpdFontFamily::Style style) const { + if (text == nullptr || *text == '\0') { + return; + } + + const auto fontIt = fontMap.find(fontId); + if (fontIt == fontMap.end()) { + LOG_ERR("GFX", "Font %d not found", fontId); + return; + } + + const auto& font = fontIt->second; + + int32_t yPosFP = fp4::fromPixel(y); + int lastBaseY = y; + int lastBaseAdvanceFP = 0; + int lastBaseTop = 0; + constexpr int MIN_COMBINING_GAP_PX = 1; + + uint32_t cp; + uint32_t prevCp = 0; + while ((cp = utf8NextCodepoint(reinterpret_cast(&text)))) { + if (utf8IsCombiningMark(cp)) { + const EpdGlyph* combiningGlyph = font.getGlyph(cp, style); + int raiseBy = 0; + if (combiningGlyph) { + const int currentGap = combiningGlyph->top - combiningGlyph->height - lastBaseTop; + if (currentGap < MIN_COMBINING_GAP_PX) { + raiseBy = MIN_COMBINING_GAP_PX - currentGap; + } + } + + const int combiningX = x + raiseBy; + const int combiningY = lastBaseY + fp4::toPixel(lastBaseAdvanceFP / 2); + renderCharImpl(*this, renderMode, font, cp, combiningX, combiningY, black, style); + continue; + } + + cp = font.applyLigatures(cp, text, style); + if (prevCp != 0) { + yPosFP += font.getKerning(prevCp, cp, style); // add for CCW (opposite of CW) + } + + lastBaseY = fp4::toPixel(yPosFP); + const EpdGlyph* glyph = font.getGlyph(cp, style); + + lastBaseAdvanceFP = glyph ? glyph->advanceX : 0; + lastBaseTop = glyph ? glyph->top : 0; + + renderCharImpl(*this, renderMode, font, cp, x, lastBaseY, black, style); + if (glyph) { + yPosFP += glyph->advanceX; // add for CCW (opposite of CW) + } + prevCp = cp; + } +} + uint8_t* GfxRenderer::getFrameBuffer() const { return frameBuffer; } size_t GfxRenderer::getBufferSize() { return HalDisplay::BUFFER_SIZE; } diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index f94fbcbd..3d5fc0a1 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -135,9 +135,11 @@ class GfxRenderer { std::vector wrappedText(int fontId, const char* text, int maxWidth, int maxLines, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; - // Helper for drawing rotated text (90 degrees clockwise, for side buttons) + // Helpers for drawing rotated text (for side buttons) void drawTextRotated90CW(int fontId, int x, int y, const char* text, bool black = true, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; + void drawTextRotated90CCW(int fontId, int x, int y, const char* text, bool black = true, + EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; int getTextHeight(int fontId) const; // Grayscale functions diff --git a/lib/Logging/Logging.cpp b/lib/Logging/Logging.cpp index d7f83606..a8ca529a 100644 --- a/lib/Logging/Logging.cpp +++ b/lib/Logging/Logging.cpp @@ -10,7 +10,10 @@ RTC_NOINIT_ATTR char logMessages[MAX_LOG_LINES][MAX_ENTRY_LEN]; RTC_NOINIT_ATTR size_t logHead = 0; void addToLogRingBuffer(const char* message) { - // Add the message to the ring buffer, overwriting old messages if necessary + // RTC_NOINIT memory may contain garbage after flash erase or power loss + if (logHead >= MAX_LOG_LINES) { + logHead = 0; + } strncpy(logMessages[logHead], message, MAX_ENTRY_LEN - 1); logMessages[logHead][MAX_ENTRY_LEN - 1] = '\0'; logHead = (logHead + 1) % MAX_LOG_LINES; diff --git a/open-x4-sdk b/open-x4-sdk index 91e7e2be..9f76376a 160000 --- a/open-x4-sdk +++ b/open-x4-sdk @@ -1 +1 @@ -Subproject commit 91e7e2bef7df514abc7b50aef763d0965abc00a6 +Subproject commit 9f76376a5cc7894cff9ca87bbdd34dab715d8a59 diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index f5a2c048..cfe83793 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -53,6 +53,15 @@ void RecentBooksStore::updateBook(const std::string& path, const std::string& ti } } +void RecentBooksStore::removeBook(const std::string& path) { + auto it = + std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); + if (it != recentBooks.end()) { + recentBooks.erase(it); + saveToFile(); + } +} + bool RecentBooksStore::saveToFile() const { Storage.mkdir("/.crosspoint"); return JsonSettingsIO::saveRecentBooks(*this, RECENT_BOOKS_FILE_JSON); diff --git a/src/RecentBooksStore.h b/src/RecentBooksStore.h index 5d98ce83..4d2d4c4f 100644 --- a/src/RecentBooksStore.h +++ b/src/RecentBooksStore.h @@ -37,6 +37,8 @@ class RecentBooksStore { void updateBook(const std::string& path, const std::string& title, const std::string& author, const std::string& coverBmpPath); + void removeBook(const std::string& path); + // Get the list of recent books (most recent first) const std::vector& getBooks() const { return recentBooks; } diff --git a/src/activities/reader/DictionaryDefinitionActivity.cpp b/src/activities/reader/DictionaryDefinitionActivity.cpp index d720488c..ec709894 100644 --- a/src/activities/reader/DictionaryDefinitionActivity.cpp +++ b/src/activities/reader/DictionaryDefinitionActivity.cpp @@ -4,7 +4,7 @@ #include -#include "ActivityResult.h" +#include "activities/ActivityResult.h" #include #include diff --git a/src/activities/reader/DictionarySuggestionsActivity.cpp b/src/activities/reader/DictionarySuggestionsActivity.cpp index 8150675e..8b501e43 100644 --- a/src/activities/reader/DictionarySuggestionsActivity.cpp +++ b/src/activities/reader/DictionarySuggestionsActivity.cpp @@ -5,7 +5,7 @@ #include "DictionaryDefinitionActivity.h" #include "HalDisplay.h" #include "MappedInputManager.h" -#include "RenderLock.h" +#include "activities/RenderLock.h" #include "components/UITheme.h" #include "fontIds.h" #include "util/Dictionary.h" @@ -56,9 +56,9 @@ void DictionarySuggestionsActivity::loop() { startActivityForResult( std::make_unique(renderer, mappedInput, selected, definition, readerFontId, orientation, true), - [this](const ActivityResult& result) { + [this](ActivityResult result) { if (!result.isCancelled) { - setResult(result); + setResult(std::move(result)); finish(); } else { requestUpdate(); diff --git a/src/activities/reader/DictionaryWordSelectActivity.cpp b/src/activities/reader/DictionaryWordSelectActivity.cpp index dfd0f2ba..ac5631af 100644 --- a/src/activities/reader/DictionaryWordSelectActivity.cpp +++ b/src/activities/reader/DictionaryWordSelectActivity.cpp @@ -5,7 +5,7 @@ #include #include -#include "ActivityResult.h" +#include "activities/ActivityResult.h" #include "CrossPointSettings.h" #include "DictionaryDefinitionActivity.h" #include "DictionarySuggestionsActivity.h" @@ -353,9 +353,9 @@ void DictionaryWordSelectActivity::loop() { startActivityForResult( std::make_unique(renderer, mappedInput, cleaned, definition, fontId, orientation, true), - [this](const ActivityResult& result) { + [this](ActivityResult result) { if (!result.isCancelled) { - setResult(result); + setResult(std::move(result)); finish(); } else { requestUpdate(); @@ -372,9 +372,9 @@ void DictionaryWordSelectActivity::loop() { startActivityForResult( std::make_unique(renderer, mappedInput, stem, stemDef, fontId, orientation, true), - [this](const ActivityResult& result) { + [this](ActivityResult result) { if (!result.isCancelled) { - setResult(result); + setResult(std::move(result)); finish(); } else { requestUpdate(); @@ -390,9 +390,9 @@ void DictionaryWordSelectActivity::loop() { startActivityForResult( std::make_unique(renderer, mappedInput, cleaned, similar, fontId, orientation, cachePath), - [this](const ActivityResult& result) { + [this](ActivityResult result) { if (!result.isCancelled) { - setResult(result); + setResult(std::move(result)); finish(); } else { requestUpdate(); diff --git a/src/activities/reader/EndOfBookMenuActivity.cpp b/src/activities/reader/EndOfBookMenuActivity.cpp index bc739ad7..08deed99 100644 --- a/src/activities/reader/EndOfBookMenuActivity.cpp +++ b/src/activities/reader/EndOfBookMenuActivity.cpp @@ -3,7 +3,7 @@ #include #include -#include "ActivityResult.h" +#include "activities/ActivityResult.h" #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index efeb991e..cdb3f3ad 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -717,7 +717,7 @@ bool EpubReaderActivity::silentIndexNextChapterIfNeeded() { marginTop += SETTINGS.screenMargin; marginLeft += SETTINGS.screenMargin; marginRight += SETTINGS.screenMargin; - marginBottom += std::max(SETTINGS.screenMargin, UITheme::getInstance().getStatusBarHeight()); + marginBottom += std::max(static_cast(SETTINGS.screenMargin), UITheme::getInstance().getStatusBarHeight()); const uint16_t vpWidth = renderer.getScreenWidth() - marginLeft - marginRight; const uint16_t vpHeight = renderer.getScreenHeight() - marginTop - marginBottom; diff --git a/src/activities/reader/EpubReaderBookmarkSelectionActivity.cpp b/src/activities/reader/EpubReaderBookmarkSelectionActivity.cpp index de3025e6..cfd4622e 100644 --- a/src/activities/reader/EpubReaderBookmarkSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderBookmarkSelectionActivity.cpp @@ -2,7 +2,7 @@ #include -#include "ActivityResult.h" +#include "activities/ActivityResult.h" #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" diff --git a/src/activities/reader/LookedUpWordsActivity.cpp b/src/activities/reader/LookedUpWordsActivity.cpp index df2be3fc..7b41c465 100644 --- a/src/activities/reader/LookedUpWordsActivity.cpp +++ b/src/activities/reader/LookedUpWordsActivity.cpp @@ -5,7 +5,7 @@ #include -#include "ActivityResult.h" +#include "activities/ActivityResult.h" #include "DictionaryDefinitionActivity.h" #include "DictionarySuggestionsActivity.h" #include "MappedInputManager.h" @@ -143,9 +143,9 @@ void LookedUpWordsActivity::loop() { startActivityForResult( std::make_unique(renderer, mappedInput, headword, definition, readerFontId, orientation, true), - [this](const ActivityResult& result) { + [this](ActivityResult result) { if (!result.isCancelled) { - setResult(result); + setResult(std::move(result)); finish(); } else { requestUpdate(); @@ -162,9 +162,9 @@ void LookedUpWordsActivity::loop() { startActivityForResult( std::make_unique(renderer, mappedInput, stem, stemDef, readerFontId, orientation, true), - [this](const ActivityResult& result) { + [this](ActivityResult result) { if (!result.isCancelled) { - setResult(result); + setResult(std::move(result)); finish(); } else { requestUpdate(); @@ -180,9 +180,9 @@ void LookedUpWordsActivity::loop() { startActivityForResult( std::make_unique(renderer, mappedInput, headword, similar, readerFontId, orientation, cachePath), - [this](const ActivityResult& result) { + [this](ActivityResult result) { if (!result.isCancelled) { - setResult(result); + setResult(std::move(result)); finish(); } else { requestUpdate(); diff --git a/src/activities/settings/NtpSyncActivity.cpp b/src/activities/settings/NtpSyncActivity.cpp index bb7d7f85..5856bfb3 100644 --- a/src/activities/settings/NtpSyncActivity.cpp +++ b/src/activities/settings/NtpSyncActivity.cpp @@ -5,7 +5,7 @@ #include #include -#include "ActivityResult.h" +#include "activities/ActivityResult.h" #include "CrossPointSettings.h" #include "MappedInputManager.h" #include "activities/network/WifiSelectionActivity.h" diff --git a/src/activities/settings/OpdsServerListActivity.cpp b/src/activities/settings/OpdsServerListActivity.cpp index 16c26a5b..63c61d49 100644 --- a/src/activities/settings/OpdsServerListActivity.cpp +++ b/src/activities/settings/OpdsServerListActivity.cpp @@ -3,7 +3,7 @@ #include #include -#include "ActivityResult.h" +#include "activities/ActivityResult.h" #include "MappedInputManager.h" #include "OpdsServerStore.h" #include "OpdsSettingsActivity.h" diff --git a/src/activities/settings/OpdsSettingsActivity.cpp b/src/activities/settings/OpdsSettingsActivity.cpp index ec54bcc5..0b1250c3 100644 --- a/src/activities/settings/OpdsSettingsActivity.cpp +++ b/src/activities/settings/OpdsSettingsActivity.cpp @@ -6,7 +6,7 @@ #include #include -#include "ActivityResult.h" +#include "activities/ActivityResult.h" #include "MappedInputManager.h" #include "OpdsServerStore.h" #include "activities/util/DirectoryPickerActivity.h" diff --git a/src/activities/util/DirectoryPickerActivity.cpp b/src/activities/util/DirectoryPickerActivity.cpp index e1ce0040..4b7d0522 100644 --- a/src/activities/util/DirectoryPickerActivity.cpp +++ b/src/activities/util/DirectoryPickerActivity.cpp @@ -5,7 +5,7 @@ #include -#include "ActivityResult.h" +#include "activities/ActivityResult.h" #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" diff --git a/src/activities/util/NumericStepperActivity.cpp b/src/activities/util/NumericStepperActivity.cpp index 023ac974..c6f5732a 100644 --- a/src/activities/util/NumericStepperActivity.cpp +++ b/src/activities/util/NumericStepperActivity.cpp @@ -6,7 +6,7 @@ #include #include -#include "ActivityResult.h" +#include "activities/ActivityResult.h" #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" diff --git a/src/util/Dictionary.h b/src/util/Dictionary.h index b7bf89d8..6c3cd43c 100644 --- a/src/util/Dictionary.h +++ b/src/util/Dictionary.h @@ -1,11 +1,11 @@ #pragma once +#include + #include #include #include #include -class FsFile; - class Dictionary { public: static bool exists(); diff --git a/src/util/StringUtils.cpp b/src/util/StringUtils.cpp index 81be07ee..5ad2902f 100644 --- a/src/util/StringUtils.cpp +++ b/src/util/StringUtils.cpp @@ -2,6 +2,10 @@ #include +#include +#include +#include + namespace StringUtils { std::string sanitizeFilename(const std::string& name, size_t maxBytes) { @@ -43,4 +47,48 @@ std::string sanitizeFilename(const std::string& name, size_t maxBytes) { return result.empty() ? "book" : result; } +bool checkFileExtension(const std::string& fileName, const char* extension) { + const size_t extLen = strlen(extension); + if (fileName.length() < extLen) return false; + const std::string tail = fileName.substr(fileName.length() - extLen); + for (size_t i = 0; i < extLen; i++) { + if (tolower(static_cast(tail[i])) != tolower(static_cast(extension[i]))) return false; + } + return true; +} + +void sortFileList(std::vector& entries) { + std::sort(entries.begin(), entries.end(), [](const std::string& a, const std::string& b) { + bool isDir1 = !a.empty() && a.back() == '/'; + bool isDir2 = !b.empty() && b.back() == '/'; + if (isDir1 != isDir2) return isDir1; + + const char* s1 = a.c_str(); + const char* s2 = b.c_str(); + + while (*s1 && *s2) { + if (isdigit(*s1) && isdigit(*s2)) { + while (*s1 == '0') s1++; + while (*s2 == '0') s2++; + int len1 = 0, len2 = 0; + while (isdigit(s1[len1])) len1++; + while (isdigit(s2[len2])) len2++; + if (len1 != len2) return len1 < len2; + for (int i = 0; i < len1; i++) { + if (s1[i] != s2[i]) return s1[i] < s2[i]; + } + s1 += len1; + s2 += len2; + } else { + char c1 = tolower(*s1); + char c2 = tolower(*s2); + if (c1 != c2) return c1 < c2; + s1++; + s2++; + } + } + return *s1 == 0 && *s2 != 0; + }); +} + } // namespace StringUtils diff --git a/src/util/StringUtils.h b/src/util/StringUtils.h index 1fa6cc01..25f2a1db 100644 --- a/src/util/StringUtils.h +++ b/src/util/StringUtils.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace StringUtils { @@ -11,4 +12,15 @@ namespace StringUtils { */ std::string sanitizeFilename(const std::string& name, size_t maxBytes = 100); +/** + * Case-insensitive file extension check. + */ +bool checkFileExtension(const std::string& fileName, const char* extension); + +/** + * Sort a file/directory list with directories first, using case-insensitive natural sort. + * Directory entries are identified by a trailing '/'. + */ +void sortFileList(std::vector& entries); + } // namespace StringUtils