## Summary This PR introduces **Focus Reading**, a generic implementation of artificial fixation points (similar to Bionic Reading) designed to improve reading speed and focus by bolding the initial characters of words. This is achieved by dynamically bolding characters during indexing. <img width="500" alt="Focus Reading on X3" src="https://github.com/user-attachments/assets/94a632a5-82da-47be-957c-538b35bf84d9" /> ### Implementation Details #### Core Text Engine (`ParsedText`) - Modified `ParsedText::addWord` to implement a custom bolding algorithm. It uses a 45% ratio for bolding, with a minimum of 1 character and a maximum of 9. - UTF-8 Safety: Integrated `utf8NextCodepoint` to ensure character counting and string slicing occur at safe byte boundaries, preventing corruption of multi-byte characters (e.g., accented letters or smart quotes). - Intelligent Tokenization: This correctly identifies and separates "word" characters (letters, apostrophes, hyphens) from "non-word" characters (numbers, brackets, smart quotes). - Formatting Preservation: The logic ensures that punctuation is not "stolen" for the bolding count and that existing styles (like italics or underlines) are preserved across the bold/regular split. - Processing at indexing stage reduces CPU load at render-time and ensures layout/fit is unaffected. - Split details are tracked with `wordIsFocusSuffix`. After splitting and layout, suffixes are merged back into their preceding word entries to prevent a doubling of RAM usage. #### Settings and UI - Version Management: Bumped `SECTION_FILE_VERSION` to `21` - Global Settings: Added `focusReadingEnabled` to `CrossPointSettings`. - User Interface: Added a new toggle in the "Reader" section of the settings menu, positioned after the "Embedded Style" option. - Localization: Added the `STR_FOCUS_READING` string #### Plumbing - Plumbed the `focusReadingEnabled` boolean through `EpubReaderActivity`, `Section`, and `ChapterHtmlSlimParser` to ensure the user's setting reaches the `ParsedText` constructor during chapter indexing. ## Additional Context ### Files Changed - `lib/Epub/Epub/ParsedText.h / .cpp`: Core fixation logic and UTF-8 tokenization. - `lib/Epub/Epub/Section.h / .cpp`: Cache header updates and invalidation logic. - `lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h / .cpp`: Plumbing the setting to text blocks. - `src/CrossPointSettings.h`: Data persistence for the new setting. - `src/SettingsList.h`: UI toggle implementation. - `src/activities/reader/EpubReaderActivity.cpp`: Handling settings changes during reading sessions. - `lib/I18n/translations/*.yaml`: UI strings. --- ### 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? _**YES**_
53 lines
2.4 KiB
C++
53 lines
2.4 KiB
C++
#pragma once
|
|
#include <EpdFontFamily.h>
|
|
#include <HalStorage.h>
|
|
|
|
#include <memory>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "Block.h"
|
|
#include "BlockStyle.h"
|
|
|
|
// Represents a line of text on a page
|
|
class TextBlock final : public Block {
|
|
private:
|
|
std::vector<std::string> words;
|
|
std::vector<int16_t> wordXpos;
|
|
std::vector<EpdFontFamily::Style> wordStyles;
|
|
// Per-word focus boundary: N > 0 means the first N bytes of words[i] are rendered bold,
|
|
// the remainder in the base style. 0 means no split (whole word uses wordStyles[i]).
|
|
// N encodes the bold PREFIX length only — bounded to 9 codepoints (≤36 UTF-8 bytes) by
|
|
// FOCUS_READING_PERCENT's 1..9 clamp in ParsedText::addWord, so it always fits in uint8_t.
|
|
// Vector is empty when no focus splits exist anywhere in the block (zero per-word RAM cost
|
|
// when focus reading is disabled, or on lines that happen to contain no splittable words).
|
|
std::vector<uint8_t> wordFocusBoundary;
|
|
// Pre-computed pixel offset from word start to the regular suffix, stored when boundary > 0.
|
|
// Eliminates getTextAdvanceX from the render path. 0 when boundary == 0.
|
|
// Empty in lockstep with wordFocusBoundary.
|
|
std::vector<uint16_t> wordFocusSuffixX;
|
|
BlockStyle blockStyle;
|
|
|
|
public:
|
|
explicit TextBlock(std::vector<std::string> words, std::vector<int16_t> word_xpos,
|
|
std::vector<EpdFontFamily::Style> word_styles, std::vector<uint8_t> focus_boundary,
|
|
std::vector<uint16_t> focus_suffix_x, const BlockStyle& blockStyle = BlockStyle())
|
|
: words(std::move(words)),
|
|
wordXpos(std::move(word_xpos)),
|
|
wordStyles(std::move(word_styles)),
|
|
wordFocusBoundary(std::move(focus_boundary)),
|
|
wordFocusSuffixX(std::move(focus_suffix_x)),
|
|
blockStyle(blockStyle) {}
|
|
~TextBlock() override = default;
|
|
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
|
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
|
const std::vector<std::string>& getWords() const { return words; }
|
|
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
|
|
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
|
BlockType getType() override { return TEXT_BLOCK; }
|
|
bool serialize(FsFile& file) const;
|
|
static std::unique_ptr<TextBlock> deserialize(FsFile& file);
|
|
};
|