From cefed0f6ce72000609f70e30427fd741f4fe3990 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 30 Apr 2026 09:11:49 +0200 Subject: [PATCH] Yet another bionic reading implementation Co-authored-by: Copilot --- lib/Epub/Epub/ParsedText.h | 9 ++++-- lib/I18n/translations/english.yaml | 2 ++ src/CrossPointSettings.h | 1 + src/JsonSettingsIO.cpp | 2 ++ src/RecentBooksStore.cpp | 14 +++++---- src/RecentBooksStore.h | 5 ++++ src/SettingsList.h | 2 +- src/activities/ActivityResult.h | 1 + src/activities/reader/EpubReaderActivity.cpp | 14 +++++---- src/activities/reader/EpubReaderActivity.h | 3 +- .../reader/EpubReaderMenuActivity.cpp | 29 ++++++++++++------- .../reader/EpubReaderMenuActivity.h | 5 ++-- 12 files changed, 61 insertions(+), 26 deletions(-) diff --git a/lib/Epub/Epub/ParsedText.h b/lib/Epub/Epub/ParsedText.h index 6aa7e3cc..2d94ed09 100644 --- a/lib/Epub/Epub/ParsedText.h +++ b/lib/Epub/Epub/ParsedText.h @@ -26,8 +26,10 @@ class ParsedText { BlockStyle blockStyle; bool extraParagraphSpacing; bool hyphenationEnabled; + bool bionicReadingEnabled; void applyParagraphIndent(); + void applyBionicReadingTransform(); std::vector computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, std::vector& wordWidths, std::vector& continuesVec); std::vector computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, @@ -60,8 +62,11 @@ class ParsedText { public: explicit ParsedText(const bool extraParagraphSpacing, const bool hyphenationEnabled = false, - const BlockStyle& blockStyle = BlockStyle()) - : blockStyle(blockStyle), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {} + const BlockStyle& blockStyle = BlockStyle(), const bool bionicReadingEnabled = false) + : blockStyle(blockStyle), + extraParagraphSpacing(extraParagraphSpacing), + hyphenationEnabled(hyphenationEnabled), + bionicReadingEnabled(bionicReadingEnabled) {} ~ParsedText() = default; void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false, bool attachToPrevious = false); diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index c667b1a7..c214eb27 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -598,3 +598,5 @@ STR_KB_HINT_SECONDARY_CHAR: "Hold SELECT for secondary char" STR_KB_HINT_UPPER_SECONDARY: "Hold SELECT for UPPERCASE or secondary char" STR_KB_HINT_LOWER_SECONDARY: "Hold SELECT for lowercase or secondary char" STR_KB_HINT_URL_SNIPPETS: "Press URL for snippets" +STR_BIONIC_READING: "Bionic Reading" +STR_BTN_ACT_TOGGLE_BIONIC_READING: "Toggle Bionic Reading" \ No newline at end of file diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index f124c030..4f1cd376 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -284,6 +284,7 @@ class CrossPointSettings { BTN_PREV_SECTION, BTN_EXIT_READER, BTN_READER_MENU, + BTN_TOGGLE_BIONIC_READING, BTN_KOREADER_SYNC, BUTTON_ACTION_COUNT }; diff --git a/src/JsonSettingsIO.cpp b/src/JsonSettingsIO.cpp index 2878ae99..d0f586b4 100644 --- a/src/JsonSettingsIO.cpp +++ b/src/JsonSettingsIO.cpp @@ -418,6 +418,7 @@ bool JsonSettingsIO::saveRecentBooks(const RecentBooksStore& store, const char* obj["imageRenderingOverride"] = book.imageRenderingOverride; obj["fontFamilyOverride"] = book.fontFamilyOverride; obj["fontSizeOverride"] = book.fontSizeOverride; + obj["bionicReadingOverride"] = book.bionicReadingOverride; } String json; @@ -455,6 +456,7 @@ bool JsonSettingsIO::loadRecentBooks(RecentBooksStore& store, const char* json) book.fontFamilyOverride = clampInt8(obj["fontFamilyOverride"] | -1, -1, CrossPointSettings::FONT_FAMILY_COUNT - 1, -1); book.fontSizeOverride = clampInt8(obj["fontSizeOverride"] | -1, -1, CrossPointSettings::FONT_SIZE_COUNT - 1, -1); + book.bionicReadingOverride = obj["bionicReadingOverride"] | false; store.recentBooks.push_back(book); } diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 8fbadd06..5891234c 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -26,6 +26,7 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title int8_t imageRenderingOverride = -1; int8_t fontFamilyOverride = -1; int8_t fontSizeOverride = -1; + bool bionicReadingOverride = false; // Remove existing entry if present auto it = @@ -35,12 +36,14 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title imageRenderingOverride = it->imageRenderingOverride; fontFamilyOverride = it->fontFamilyOverride; fontSizeOverride = it->fontSizeOverride; + bionicReadingOverride = it->bionicReadingOverride; recentBooks.erase(it); } // Add to front - recentBooks.insert(recentBooks.begin(), {path, title, author, series, coverBmpPath, embeddedStyleOverride, - imageRenderingOverride, fontFamilyOverride, fontSizeOverride}); + recentBooks.insert(recentBooks.begin(), + {path, title, author, series, coverBmpPath, embeddedStyleOverride, imageRenderingOverride, + fontFamilyOverride, fontSizeOverride, bionicReadingOverride}); // Trim to max size if (recentBooks.size() > MAX_RECENT_BOOKS) { @@ -83,19 +86,19 @@ RecentBook RecentBooksStore::getBookByPath(const std::string& path) const { } bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride, - const int8_t imageRenderingOverride) { + const int8_t imageRenderingOverride, const bool bionicReadingOverride) { auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); if (it == recentBooks.end()) { return false; } return setReaderOverrides(path, embeddedStyleOverride, imageRenderingOverride, it->fontFamilyOverride, - it->fontSizeOverride); + it->fontSizeOverride, bionicReadingOverride); } bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t embeddedStyleOverride, const int8_t imageRenderingOverride, const int8_t fontFamilyOverride, - const int8_t fontSizeOverride) { + const int8_t fontSizeOverride, const bool bionicReadingOverride) { auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); if (it == recentBooks.end()) { @@ -106,6 +109,7 @@ bool RecentBooksStore::setReaderOverrides(const std::string& path, const int8_t it->imageRenderingOverride = imageRenderingOverride; it->fontFamilyOverride = fontFamilyOverride; it->fontSizeOverride = fontSizeOverride; + it->bionicReadingOverride = bionicReadingOverride; return saveToFile(); } diff --git a/src/RecentBooksStore.h b/src/RecentBooksStore.h index 6ca8f7a6..aa381153 100644 --- a/src/RecentBooksStore.h +++ b/src/RecentBooksStore.h @@ -17,6 +17,7 @@ struct RecentBook { int8_t fontFamilyOverride = -1; // -1 = use global setting, otherwise CrossPointSettings::FONT_SIZE value. int8_t fontSizeOverride = -1; + bool bionicReadingOverride = false; bool operator==(const RecentBook& other) const { return path == other.path; } }; @@ -64,6 +65,10 @@ class RecentBooksStore { bool setReaderOverrides(const std::string& path, int8_t embeddedStyleOverride, int8_t imageRenderingOverride); bool setReaderOverrides(const std::string& path, int8_t embeddedStyleOverride, int8_t imageRenderingOverride, int8_t fontFamilyOverride, int8_t fontSizeOverride); + bool setReaderOverrides(const std::string& path, int8_t embeddedStyleOverride, int8_t imageRenderingOverride, + bool bionicReadingOverride); + bool setReaderOverrides(const std::string& path, int8_t embeddedStyleOverride, int8_t imageRenderingOverride, + int8_t fontFamilyOverride, int8_t fontSizeOverride, bool bionicReadingOverride); private: bool loadFromBinaryFile(); diff --git a/src/SettingsList.h b/src/SettingsList.h index e53acd96..1f7149ec 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -145,7 +145,7 @@ inline const std::vector list = { StrId::STR_BTN_ACT_FORCE_REFRESH, StrId::STR_BTN_ACT_OPEN_TOC, StrId::STR_BTN_ACT_OPEN_BOOKMARKS, \ StrId::STR_BTN_ACT_STAR_PAGE, StrId::STR_BTN_ACT_FOOTNOTES, StrId::STR_BTN_ACT_NEXT_SECTION, \ StrId::STR_BTN_ACT_PREV_SECTION, StrId::STR_BTN_ACT_EXIT_READER, StrId::STR_BTN_ACT_READER_MENU, \ - StrId::STR_BTN_ACT_KOREADER_SYNC + StrId::STR_BTN_ACT_TOGGLE_BIONIC_READING, StrId::STR_BTN_ACT_KOREADER_SYNC // Back button: short=exit reader, double=ignore, long=go home SettingInfo::Enum(StrId::STR_BTN_SHORT_PRESS, &CrossPointSettings::btnShortBack, {StrId::STR_BTN_DEF_EXIT_READER}, diff --git a/src/activities/ActivityResult.h b/src/activities/ActivityResult.h index 151137b6..c535d671 100644 --- a/src/activities/ActivityResult.h +++ b/src/activities/ActivityResult.h @@ -28,6 +28,7 @@ struct MenuResult { int8_t fontFamilyOverride = -1; int8_t fontSizeOverride = -1; uint8_t textDarkness = 1; + uint8_t bionicReadingOverride = 0; }; struct ChapterResult { diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index c678bc14..490dab4f 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -191,6 +191,7 @@ void EpubReaderActivity::onEnter() { bookImageRenderingOverride = currentBook.imageRenderingOverride; bookFontFamilyOverride = currentBook.fontFamilyOverride; bookFontSizeOverride = currentBook.fontSizeOverride; + bookBionicReadingOverride = currentBook.bionicReadingOverride; logReaderMemSnapshot("onEnter_after_recent_books"); // Trigger first update @@ -1033,13 +1034,14 @@ void EpubReaderActivity::toggleAutoPageTurn(const uint8_t selectedPageTurnOption void EpubReaderActivity::applyBookReaderOverrides(const int8_t embeddedStyleOverride, const int8_t imageRenderingOverride, const int8_t fontFamilyOverride, - const int8_t fontSizeOverride) { + const int8_t fontSizeOverride, const bool bionicReadingOverride) { if (!epub) { return; } if (bookEmbeddedStyleOverride == embeddedStyleOverride && bookImageRenderingOverride == imageRenderingOverride && - bookFontFamilyOverride == fontFamilyOverride && bookFontSizeOverride == fontSizeOverride) { + bookFontFamilyOverride == fontFamilyOverride && bookFontSizeOverride == fontSizeOverride && + bookBionicReadingOverride == bionicReadingOverride) { return; } @@ -1047,8 +1049,9 @@ void EpubReaderActivity::applyBookReaderOverrides(const int8_t embeddedStyleOver bookImageRenderingOverride = imageRenderingOverride; bookFontFamilyOverride = fontFamilyOverride; bookFontSizeOverride = fontSizeOverride; + bookBionicReadingOverride = bionicReadingOverride; RECENT_BOOKS.setReaderOverrides(epub->getPath(), bookEmbeddedStyleOverride, bookImageRenderingOverride, - bookFontFamilyOverride, bookFontSizeOverride); + bookFontFamilyOverride, bookFontSizeOverride, bookBionicReadingOverride); RenderLock lock(*this); if (section) { @@ -1861,14 +1864,15 @@ void EpubReaderActivity::openReaderMenu() { std::make_unique( renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation, !currentPageFootnotes.empty(), bookEmbeddedStyleOverride, bookImageRenderingOverride, bookFontFamilyOverride, - bookFontSizeOverride, SETTINGS.textDarkness, !bookmarkStore.isEmpty(), isCurrentPageStarred), + bookFontSizeOverride, SETTINGS.textDarkness, bookBionicReadingOverride, !bookmarkStore.isEmpty(), + isCurrentPageStarred), [this](const ActivityResult& result) { const auto& menu = std::get(result.data); applyOrientation(menu.orientation); applyTextDarkness(menu.textDarkness); toggleAutoPageTurn(menu.pageTurnOption); applyBookReaderOverrides(menu.embeddedStyleOverride, menu.imageRenderingOverride, menu.fontFamilyOverride, - menu.fontSizeOverride); + menu.fontSizeOverride, static_cast(menu.bionicReadingOverride)); if (!result.isCancelled) { onReaderMenuConfirm(static_cast(menu.action)); } diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 1fa1789b..37429356 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -135,6 +135,7 @@ class EpubReaderActivity final : public Activity { int8_t bookImageRenderingOverride = -1; int8_t bookFontFamilyOverride = -1; int8_t bookFontSizeOverride = -1; + bool bookBionicReadingOverride = false; // Bookmarks (starred pages) BookmarkStore bookmarkStore; @@ -176,7 +177,7 @@ class EpubReaderActivity final : public Activity { void applyTextDarkness(uint8_t textDarkness); void toggleAutoPageTurn(uint8_t selectedPageTurnOption); void applyBookReaderOverrides(int8_t embeddedStyleOverride, int8_t imageRenderingOverride, int8_t fontFamilyOverride, - int8_t fontSizeOverride); + int8_t fontSizeOverride, bool bionicReadingOverride); void openReaderMenu(); bool getEffectiveEmbeddedStyle() const; uint8_t getEffectiveImageRendering() const; diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 8f42e97e..44524993 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -30,14 +30,12 @@ std::string defaultFontFamilyLabel(const SettingInfo& item) { } } // namespace -EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::string& title, const int currentPage, const int totalPages, - const int bookProgressPercent, const uint8_t currentOrientation, - const bool hasFootnotes, const int8_t initialEmbeddedStyleOverride, - const int8_t initialImageRenderingOverride, - const int8_t initialFontFamilyOverride, - const int8_t initialFontSizeOverride, const uint8_t initialTextDarkness, - const bool hasStarredPages, const bool isCurrentPageStarred) +EpubReaderMenuActivity::EpubReaderMenuActivity( + GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const int currentPage, + const int totalPages, const int bookProgressPercent, const uint8_t currentOrientation, const bool hasFootnotes, + const int8_t initialEmbeddedStyleOverride, const int8_t initialImageRenderingOverride, + const int8_t initialFontFamilyOverride, const int8_t initialFontSizeOverride, const uint8_t initialTextDarkness, + const bool initialBionicReadingOverride, const bool hasStarredPages, const bool isCurrentPageStarred) : MenuListActivity("EpubReaderMenu", renderer, mappedInput), currentPageStarred(isCurrentPageStarred), pendingOrientation(currentOrientation), @@ -46,6 +44,7 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu pendingFontFamilyOverride(initialFontFamilyOverride), pendingFontSizeOverride(initialFontSizeOverride), pendingTextDarkness(initialTextDarkness), + pendingBionicReading(initialBionicReadingOverride), title(title), currentPage(currentPage), totalPages(totalPages), @@ -161,6 +160,15 @@ void EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, bool hasStarredPa [](void* ctx, uint8_t v) { static_cast(ctx)->pendingTextDarkness = v; }) .withSubmenu(StrId::STR_READER_OVERRIDES)); + menuItems.push_back( + SettingInfo::DynamicEnumCtx( + StrId::STR_BIONIC_READING, {StrId::STR_STATE_OFF, StrId::STR_STATE_ON}, self, + [](const void* ctx) -> uint8_t { + return static_cast(ctx)->pendingBionicReading ? 1 : 0; + }, + [](void* ctx, uint8_t v) { static_cast(ctx)->pendingBionicReading = (v != 0); }) + .withSubmenu(StrId::STR_READER_OVERRIDES)); + // Helper functions, reading ruler, auto page turn, orientation menuItems.push_back(SettingInfo::Separator(StrId::STR_READER_UTILS)); // Auto page turn: ACTION type with custom cycling in onActionSelected @@ -240,7 +248,7 @@ EpubReaderMenuActivity::MenuAction EpubReaderMenuActivity::actionForSettingActio void EpubReaderMenuActivity::finishWithAction(MenuAction action) { setResult(MenuResult{static_cast(action), -1, pendingOrientation, selectedPageTurnOption, pendingEmbeddedStyleOverride, pendingImageRenderingOverride, pendingFontFamilyOverride, - pendingFontSizeOverride, pendingTextDarkness}); + pendingFontSizeOverride, pendingTextDarkness, static_cast(pendingBionicReading)}); finish(); } @@ -273,7 +281,8 @@ void EpubReaderMenuActivity::onBackPressed() { pendingImageRenderingOverride, pendingFontFamilyOverride, pendingFontSizeOverride, - pendingTextDarkness}; + pendingTextDarkness, + static_cast(pendingBionicReading)}; setResult(std::move(result)); finish(); } diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index df39b99c..f119b8d3 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -37,8 +37,8 @@ class EpubReaderMenuActivity final : public MenuListActivity { const uint8_t currentOrientation, const bool hasFootnotes, const int8_t initialEmbeddedStyleOverride, const int8_t initialImageRenderingOverride, const int8_t initialFontFamilyOverride, const int8_t initialFontSizeOverride, - const uint8_t initialTextDarkness, const bool hasStarredPages, - const bool isCurrentPageStarred); + const uint8_t initialTextDarkness, const bool initialBionicReadingOverride, + const bool hasStarredPages, const bool isCurrentPageStarred); void onEnter() override; void render(RenderLock&&) override; @@ -69,6 +69,7 @@ class EpubReaderMenuActivity final : public MenuListActivity { int8_t pendingFontFamilyOverride = -1; int8_t pendingFontSizeOverride = -1; uint8_t pendingTextDarkness = 1; + bool pendingBionicReading = false; static constexpr const char* pageTurnLabels[] = {"", "1", "3", "6", "12"};