Skip to content

feat: wrapped text in GfxRender, implemented in themes so far#1141

Merged
ngxson merged 14 commits intocrosspoint-reader:masterfrom
iandchasse:refactor/gfx-wrapped-text
Feb 25, 2026
Merged

feat: wrapped text in GfxRender, implemented in themes so far#1141
ngxson merged 14 commits intocrosspoint-reader:masterfrom
iandchasse:refactor/gfx-wrapped-text

Conversation

@iandchasse
Copy link
Contributor

@iandchasse iandchasse commented Feb 23, 2026

Summary

std::vector<std::string> GfxRenderer::wrappedText(const int fontId, const char* text, const int maxWidth,
                                                  const int maxLines, const EpdFontFamily::Style style) const

Applied logic to all uses in Lyra, Lyra Extended, and base theme (continue reading card as pointed out by @znelson

Additional Context

IMG_8604
IMG_8605
IMG_8606


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 >

iandchasse and others added 9 commits February 22, 2026 01:47
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…emes

Address znelson's PR review feedback on commit 10a2678:

- Add GfxRenderer::wrappedText() utility that word-wraps text into at
  most N lines, delegating last-line truncation to truncatedText for
  correct UTF-8-safe ellipsis handling
- Change ellipsis in truncatedText from ASCII '...' to UTF-8 U+2026 (…)
- Replace duplicated word-wrap loops in BaseTheme, LyraTheme, and
  Lyra3CoversTheme with single calls to wrappedText
- Replace manual author truncation loops in BaseTheme with truncatedText
- Remove now-unused Utf8.h includes from the three theme files
maxLineWidth was only defined inside the removed word-wrap loop.
Inline bookWidth - 40 directly and clean up author box-sizing logic.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 677f7b8 and 991828a.

📒 Files selected for processing (2)
  • lib/GfxRenderer/GfxRenderer.cpp
  • lib/GfxRenderer/GfxRenderer.h
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/GfxRenderer/GfxRenderer.h
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: cppcheck
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: Uri-Tauber
Repo: crosspoint-reader/crosspoint-reader PR: 988
File: lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp:649-661
Timestamp: 2026-02-19T12:17:05.421Z
Learning: In ChapterHtmlSlimParser.cpp, when computing footnote word indices in endElement() for footnote links, the wordIndex must be cumulative across the 750-word mid-paragraph flush boundary. The correct calculation is: `wordIndex = wordsExtractedInBlock + currentTextBlock->size()`, not just `currentTextBlock->size()`. This ensures footnotes attach to the page containing their actual anchor word, even after layoutAndExtractLines(false) has extracted and removed earlier words from the block.
📚 Learning: 2026-02-19T12:17:05.421Z
Learnt from: Uri-Tauber
Repo: crosspoint-reader/crosspoint-reader PR: 988
File: lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp:649-661
Timestamp: 2026-02-19T12:17:05.421Z
Learning: In ChapterHtmlSlimParser.cpp, when computing footnote word indices in endElement() for footnote links, the wordIndex must be cumulative across the 750-word mid-paragraph flush boundary. The correct calculation is: `wordIndex = wordsExtractedInBlock + currentTextBlock->size()`, not just `currentTextBlock->size()`. This ensures footnotes attach to the page containing their actual anchor word, even after layoutAndExtractLines(false) has extracted and removed earlier words from the block.

Applied to files:

  • lib/GfxRenderer/GfxRenderer.cpp
🔇 Additional comments (2)
lib/GfxRenderer/GfxRenderer.cpp (2)

833-834: UTF‑8 ellipsis update looks good.

Clear and readable, and the comment documents the encoding.


848-910: wrappedText flow looks solid.

The maxLines guard and immediate push of oversized carried-over words keep output bounded and avoid odd ellipsis+word combinations.


📝 Walkthrough

Walkthrough

Adds GfxRenderer::wrappedText(...) for UTF‑8‑safe word wrapping (maxWidth/maxLines), switches truncatedText() to use U+2026 ellipsis, and replaces manual UTF‑8 wrapping/trimming in several theme files; also adjusts a cover tile height metric to accommodate multi-line titles.

Changes

Cohort / File(s) Summary
GfxRenderer API
lib/GfxRenderer/GfxRenderer.h, lib/GfxRenderer/GfxRenderer.cpp
Introduced GfxRenderer::wrappedText(int fontId, const char* text, int maxWidth, int maxLines, EpdFontFamily::Style style) const returning std::vector<std::string> (word-wrap with final-line truncation); changed truncatedText(...) to use UTF‑8 U+2026 ellipsis; added required <string>/<vector> includes.
Theme text rendering
src/components/themes/BaseTheme.cpp, src/components/themes/lyra/LyraTheme.cpp, src/components/themes/lyra/Lyra3CoversTheme.cpp
Replaced custom per-word UTF‑8 wrapping/trimming with renderer.wrappedText() for titles and renderer.truncatedText() for authors; removed manual UTF‑8 trimming loops and Utf8.h usages; updated drawing loops and layout calculations to use wrapped/truncated results.
Layout metric
src/components/themes/lyra/Lyra3CoversTheme.h
Adjusted homeCoverTileHeight from 287 to 300 to accommodate multi-line title rendering.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • znelson
  • lukestein
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: implementing wrapped text functionality in GfxRenderer and applying it to themes.
Description check ✅ Passed The description relates to the changeset by explaining the new wrappedText function implementation and its application across themes, with supporting context and images.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/components/themes/BaseTheme.cpp (1)

513-514: Author truncation is performed twice with identical parameters.

truncatedText(UI_10_FONT_ID, lastBookAuthor.c_str(), bookWidth - 40) is called at Line 513 (for box width calculation inside if (coverRendered)) and again at Line 538 (for rendering). Since both use identical parameters, consider hoisting the result above the if (coverRendered) block to avoid redundant text measurement.

Proposed refactor
+    std::string truncatedAuthor;
+    int authorWidth = 0;
+    if (!lastBookAuthor.empty()) {
+      truncatedAuthor = renderer.truncatedText(UI_10_FONT_ID, lastBookAuthor.c_str(), bookWidth - 40);
+      authorWidth = renderer.getTextWidth(UI_10_FONT_ID, truncatedAuthor.c_str());
+    }
+
     // If cover image was rendered, draw box behind title and author
     if (coverRendered) {
       constexpr int boxPadding = 8;
       // Calculate the max text width for the box
       int maxTextWidth = 0;
       for (const auto& line : lines) {
         const int lineWidth = renderer.getTextWidth(UI_12_FONT_ID, line.c_str());
         if (lineWidth > maxTextWidth) {
           maxTextWidth = lineWidth;
         }
       }
       if (!lastBookAuthor.empty()) {
-        const auto truncatedAuthor = renderer.truncatedText(UI_10_FONT_ID, lastBookAuthor.c_str(), bookWidth - 40);
-        const int authorWidth = renderer.getTextWidth(UI_10_FONT_ID, truncatedAuthor.c_str());
         if (authorWidth > maxTextWidth) {
           maxTextWidth = authorWidth;
         }
       }
       ...
     }
     ...
     if (!lastBookAuthor.empty()) {
       titleYStart += renderer.getLineHeight(UI_10_FONT_ID) / 2;
-      auto trimmedAuthor = renderer.truncatedText(UI_10_FONT_ID, lastBookAuthor.c_str(), bookWidth - 40);
-      renderer.drawCenteredText(UI_10_FONT_ID, titleYStart, trimmedAuthor.c_str(), !bookSelected);
+      renderer.drawCenteredText(UI_10_FONT_ID, titleYStart, truncatedAuthor.c_str(), !bookSelected);
     }

Also applies to: 538-538

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/themes/BaseTheme.cpp` around lines 513 - 514, Hoist the
duplicated author truncation by computing truncatedAuthor once before the
coverRendered check and reuse it in both measurement and rendering: call
renderer.truncatedText(UI_10_FONT_ID, lastBookAuthor.c_str(), bookWidth - 40)
once into a local variable (e.g., truncatedAuthor) above the if (coverRendered)
block, then use that same truncatedAuthor for authorWidth via
renderer.getTextWidth(UI_10_FONT_ID, truncatedAuthor.c_str()) and later when
rendering; update references to avoid calling renderer.truncatedText a second
time.
lib/GfxRenderer/GfxRenderer.cpp (1)

855-893: Prefer static_cast<int> over C-style casts.

Lines 856 and 892 use (int)lines.size(). The rest of this PR (e.g., Lyra3CoversTheme.cpp Line 96, BaseTheme.cpp Line 493) consistently uses static_cast<int>(...). Consider aligning for consistency.

Proposed fix
-    if ((int)lines.size() == maxLines - 1) {
+    if (static_cast<int>(lines.size()) == maxLines - 1) {
-  if (!currentLine.empty() && (int)lines.size() < maxLines) {
+  if (!currentLine.empty() && static_cast<int>(lines.size()) < maxLines) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/GfxRenderer/GfxRenderer.cpp` around lines 855 - 893, Replace the two
C-style casts of (int)lines.size() in GfxRenderer.cpp with C++-style casts:
where the loop checks if ((int)lines.size() == maxLines - 1) and where the final
if uses (int)lines.size(), change them to static_cast<int>(lines.size()) so the
comparisons use static_cast<int>(lines.size()) == maxLines - 1 and
static_cast<int>(lines.size()) < maxLines respectively; update both occurrences
to match the project's casting style.
src/components/themes/lyra/Lyra3CoversTheme.cpp (1)

26-26: Remove unused bookTitleHeight variable.

bookTitleHeight is computed on line 26 but never referenced. The title-box rendering uses dynamicTitleBoxHeight (line 109) instead. Remove the dead code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/themes/lyra/Lyra3CoversTheme.cpp` at line 26, Remove the dead
local variable bookTitleHeight declared in Lyra3CoversTheme.cpp (the const int
bookTitleHeight = ... line); it is never referenced (rendering uses
dynamicTitleBoxHeight), so delete that declaration and any related unused
hPaddingInSelection-only computation tied solely to bookTitleHeight to avoid
unused-variable warnings; ensure no other code paths reference bookTitleHeight
before committing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/GfxRenderer/GfxRenderer.cpp`:
- Around line 878-888: The bug is that when you set currentLine = word inside
the else-branch (where currentLine was non-empty and you flushed it), you never
check whether that single-word "word" itself exceeds maxWidth, so it can later
be pushed untruncated; update the branch in GfxRenderer.cpp so after setting
currentLine = word (or instead of assigning raw word) you measure/truncate the
lone word using truncatedText(fontId, word.c_str(), maxWidth, style) whenever
that word's width exceeds maxWidth, ensuring any carried-over word placed into
currentLine is truncated immediately; keep all other logic (flushing lines,
last-line truncation) intact.

---

Nitpick comments:
In `@lib/GfxRenderer/GfxRenderer.cpp`:
- Around line 855-893: Replace the two C-style casts of (int)lines.size() in
GfxRenderer.cpp with C++-style casts: where the loop checks if
((int)lines.size() == maxLines - 1) and where the final if uses
(int)lines.size(), change them to static_cast<int>(lines.size()) so the
comparisons use static_cast<int>(lines.size()) == maxLines - 1 and
static_cast<int>(lines.size()) < maxLines respectively; update both occurrences
to match the project's casting style.

In `@src/components/themes/BaseTheme.cpp`:
- Around line 513-514: Hoist the duplicated author truncation by computing
truncatedAuthor once before the coverRendered check and reuse it in both
measurement and rendering: call renderer.truncatedText(UI_10_FONT_ID,
lastBookAuthor.c_str(), bookWidth - 40) once into a local variable (e.g.,
truncatedAuthor) above the if (coverRendered) block, then use that same
truncatedAuthor for authorWidth via renderer.getTextWidth(UI_10_FONT_ID,
truncatedAuthor.c_str()) and later when rendering; update references to avoid
calling renderer.truncatedText a second time.

In `@src/components/themes/lyra/Lyra3CoversTheme.cpp`:
- Line 26: Remove the dead local variable bookTitleHeight declared in
Lyra3CoversTheme.cpp (the const int bookTitleHeight = ... line); it is never
referenced (rendering uses dynamicTitleBoxHeight), so delete that declaration
and any related unused hPaddingInSelection-only computation tied solely to
bookTitleHeight to avoid unused-variable warnings; ensure no other code paths
reference bookTitleHeight before committing.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 13592db and b6cb748.

📒 Files selected for processing (6)
  • lib/GfxRenderer/GfxRenderer.cpp
  • lib/GfxRenderer/GfxRenderer.h
  • src/components/themes/BaseTheme.cpp
  • src/components/themes/lyra/Lyra3CoversTheme.cpp
  • src/components/themes/lyra/Lyra3CoversTheme.h
  • src/components/themes/lyra/LyraTheme.cpp
📜 Review details
🔇 Additional comments (6)
lib/GfxRenderer/GfxRenderer.h (1)

120-124: Clean API addition with good documentation.

The wrappedText declaration is well-documented, consistent with the existing truncatedText API pattern, and the default style parameter follows the convention used by other text methods in this class.

lib/GfxRenderer/GfxRenderer.cpp (1)

831-832: Good change: UTF-8 safe ellipsis.

Switching from ASCII "..." to the proper Unicode horizontal ellipsis (U+2026) ensures consistent width measurement and correct UTF-8 handling throughout the rendering pipeline.

src/components/themes/lyra/Lyra3CoversTheme.h (1)

28-28: Metric increase aligns with multi-line title support.

The bump from 287 to 300 for homeCoverTileHeight accommodates the additional vertical space needed by dynamically-sized multi-line title blocks. Looks appropriate.

src/components/themes/lyra/LyraTheme.cpp (1)

487-503: Clean migration to wrappedText API.

The manual word-splitting logic is neatly replaced with a single wrappedText call. The vertical centering calculation correctly accounts for both the title block and author line heights, and the BOLD style is consistently applied for both measurement and rendering.

src/components/themes/lyra/Lyra3CoversTheme.cpp (1)

90-117: Good dynamic title box sizing.

The multi-line wrapping with dynamic height calculation is well-implemented. The selection highlight and title rendering both correctly use the computed dynamicTitleBoxHeight based on actual content.

src/components/themes/BaseTheme.cpp (1)

490-490: Good migration to wrappedText for title rendering.

The replacement of manual word-wrapping with wrappedText simplifies the logic significantly while maintaining correct vertical centering and box sizing.

- GfxRenderer::wrappedText: truncate carried-over word immediately when it
  exceeds maxWidth, preventing it being pushed untruncated into currentLine
- GfxRenderer::wrappedText: replace C-style (int) casts with static_cast<int>
- BaseTheme::drawRecentBookCover: hoist truncatedText call for author above
  coverRendered block; reuse result for both box-size measurement and rendering
- Lyra3CoversTheme::drawRecentBookCover: remove unused bookTitleHeight variable
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/GfxRenderer/GfxRenderer.cpp (1)

881-883: ⚠️ Potential issue | 🟠 Major

Oversized word assigned to currentLine without truncation — line overflow still possible.

When currentLine is non-empty and the combined testLine exceeds maxWidth, the existing line is flushed and word becomes the new currentLine. However, if word alone is wider than maxWidth it will later be pushed untruncated — either when the next word also overflows (triggering another flush) or at the final push (Line 894–896). Only the currentLine.empty() branch (Line 884–890) and the last-line branch (Line 856–862) truncate an oversized word.

Example with maxLines = 3, maxWidth = 100, words ["Hi", "Superlongword", "End"] where "Superlongword" alone > 100 px:

  1. "Hi" fits → currentLine = "Hi"
  2. "Hi Superlongword" overflows → push "Hi", currentLine = "Superlongword" (untruncated!)
  3. "Superlongword End" overflows → push "Superlongword" (exceeds maxWidth!)
  4. Loop ends, push "End"

Result: ["Hi", "Superlongword", "End"] — line 2 overflows the render box.

🐛 Proposed fix
       if (!currentLine.empty()) {
         lines.push_back(currentLine);
-        currentLine = word;
+        if (getTextWidth(fontId, word.c_str(), style) > maxWidth) {
+          lines.push_back(truncatedText(fontId, word.c_str(), maxWidth, style));
+          currentLine.clear();
+        } else {
+          currentLine = word;
+        }
       } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/GfxRenderer/GfxRenderer.cpp` around lines 881 - 883, When flushing a
non-empty currentLine because testLine exceeds maxWidth, ensure the new
assignment currentLine = word also truncates oversized words: check the pixel
width of word against maxWidth and, if it exceeds, replace word with the same
truncation routine used elsewhere (the code path in the currentLine.empty()
branch and the final-line branch) before assigning to currentLine; update the
logic around testLine/currentLine/word to call that truncate helper (or inline
truncateToWidth behavior) so no single word pushed into lines can exceed
maxWidth.
🧹 Nitpick comments (2)
lib/GfxRenderer/GfxRenderer.cpp (1)

856-856: Nitpick: prefer static_cast<int> over C-style cast.

(int)lines.size() is used at lines 856 and 894. Prefer static_cast<int>(lines.size()) for consistency with the rest of the codebase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/GfxRenderer/GfxRenderer.cpp` at line 856, Replace the C-style casts of
lines.size() with C++ style casts: change occurrences of (int)lines.size()
(notably where used in the conditional around maxLines — currently at the checks
near lines 856 and 894 in GfxRenderer.cpp) to static_cast<int>(lines.size());
update both sites so the codebase uses static_cast consistently.
wrapped-text-refactor.patch (1)

304-310: homeCoverTileHeight bump to 300 is tied to the font metric constraint noted above.

If SMALL_FONT_ID's advanceY > 17 px the tile will still be too short for 3-line titles. Consider deriving this constant programmatically (e.g. homeCoverHeight + 2 * hPaddingInSelection + 5 + 3 * lineHeight) or at minimum add a comment documenting the font-size dependency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wrapped-text-refactor.patch` around lines 304 - 310, The hardcoded bump of
ThemeMetrics.homeCoverTileHeight to 300 depends on the SMALL_FONT_ID font metric
(advanceY) and can still be too short if SMALL_FONT_ID.advanceY > 17; update the
value to be derived programmatically (e.g. compute homeCoverTileHeight from
homeCoverHeight plus padding and 3*lineHeight using the SMALL_FONT_ID.advanceY)
or, if you keep a constant, add a clear comment next to
ThemeMetrics.homeCoverTileHeight explaining the dependency on
SMALL_FONT_ID.advanceY and why 300 was chosen; reference
ThemeMetrics.homeCoverTileHeight, SMALL_FONT_ID, SMALL_FONT_ID.advanceY,
homeCoverHeight and any hPaddingInSelection/lineHeight helpers in the
implementation so the relationship is obvious and robust to font changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wrapped-text-refactor.patch`:
- Around line 267-277: The title block can exceed the tile bottom because
SMALL_FONT_ID lineHeight (renderer.getLineHeight) times 3 is taller than the
remaining space; fix by computing the available vertical space from the title
start (use tileY and the existing offsets) and deriving maxLines =
floor(availableHeight / renderer.getLineHeight(SMALL_FONT_ID)), then clamp the
wrapped text to that max (call renderer.wrappedText with min(3, maxLines) or
truncate the result/replace with a single truncated line if maxLines==1), and
recompute dynamicBlockHeight/dynamicTitleBoxHeight from the actual number of
lines; update the use of renderer.wrappedText, getLineHeight, dynamicBlockHeight
and dynamicTitleBoxHeight accordingly so text never overflows the tile.

---

Duplicate comments:
In `@lib/GfxRenderer/GfxRenderer.cpp`:
- Around line 881-883: When flushing a non-empty currentLine because testLine
exceeds maxWidth, ensure the new assignment currentLine = word also truncates
oversized words: check the pixel width of word against maxWidth and, if it
exceeds, replace word with the same truncation routine used elsewhere (the code
path in the currentLine.empty() branch and the final-line branch) before
assigning to currentLine; update the logic around testLine/currentLine/word to
call that truncate helper (or inline truncateToWidth behavior) so no single word
pushed into lines can exceed maxWidth.

---

Nitpick comments:
In `@lib/GfxRenderer/GfxRenderer.cpp`:
- Line 856: Replace the C-style casts of lines.size() with C++ style casts:
change occurrences of (int)lines.size() (notably where used in the conditional
around maxLines — currently at the checks near lines 856 and 894 in
GfxRenderer.cpp) to static_cast<int>(lines.size()); update both sites so the
codebase uses static_cast consistently.

In `@wrapped-text-refactor.patch`:
- Around line 304-310: The hardcoded bump of ThemeMetrics.homeCoverTileHeight to
300 depends on the SMALL_FONT_ID font metric (advanceY) and can still be too
short if SMALL_FONT_ID.advanceY > 17; update the value to be derived
programmatically (e.g. compute homeCoverTileHeight from homeCoverHeight plus
padding and 3*lineHeight using the SMALL_FONT_ID.advanceY) or, if you keep a
constant, add a clear comment next to ThemeMetrics.homeCoverTileHeight
explaining the dependency on SMALL_FONT_ID.advanceY and why 300 was chosen;
reference ThemeMetrics.homeCoverTileHeight, SMALL_FONT_ID,
SMALL_FONT_ID.advanceY, homeCoverHeight and any hPaddingInSelection/lineHeight
helpers in the implementation so the relationship is obvious and robust to font
changes.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b6cb748 and 3424c06.

📒 Files selected for processing (3)
  • lib/GfxRenderer/GfxRenderer.cpp
  • open-x4-sdk
  • wrapped-text-refactor.patch
✅ Files skipped from review due to trivial changes (1)
  • open-x4-sdk
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cppcheck
  • GitHub Check: build
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-02-19T12:17:05.421Z
Learnt from: Uri-Tauber
Repo: crosspoint-reader/crosspoint-reader PR: 988
File: lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp:649-661
Timestamp: 2026-02-19T12:17:05.421Z
Learning: In ChapterHtmlSlimParser.cpp, when computing footnote word indices in endElement() for footnote links, the wordIndex must be cumulative across the 750-word mid-paragraph flush boundary. The correct calculation is: `wordIndex = wordsExtractedInBlock + currentTextBlock->size()`, not just `currentTextBlock->size()`. This ensures footnotes attach to the page containing their actual anchor word, even after layoutAndExtractLines(false) has extracted and removed earlier words from the block.

Applied to files:

  • wrapped-text-refactor.patch
  • lib/GfxRenderer/GfxRenderer.cpp
🔇 Additional comments (5)
wrapped-text-refactor.patch (3)

192-192: Clean consolidation — LGTM.

Replacing 82 lines of bespoke word-wrap logic with a single renderer.wrappedText(...) call is a significant maintainability win and the parameters (UI_12_FONT_ID, bookWidth - 40, max 3 lines) are consistent with the old implementation.


380-381: LGTM — LyraTheme.cpp simplification is clean.

Replacing 57 lines of manual word-wrap with renderer.wrappedText(UI_12_FONT_ID, book.title.c_str(), textWidth, 3, EpdFontFamily::BOLD) is correct: maxWidth = textWidth, maxLines = 3, and EpdFontFamily::BOLD matches the style that was previously used throughout the removed code.


213-218: The concern about maxLineWidth scope is not applicable to the current codebase.

The patch snippet shown in your review would have used an undefined maxLineWidth variable, but the final code that was committed takes a different approach: it pre-calculates truncatedAuthor once using bookWidth - 40 directly, then measures that pre-calculated string. The variable maxLineWidth does not exist in the current BaseTheme.cpp, and bookWidth - 40 is used inline instead. The final implementation avoids the scoping issue entirely.

Likely an incorrect or invalid review comment.

lib/GfxRenderer/GfxRenderer.cpp (2)

831-832: LGTM — single U+2026 glyph is the correct replacement for ASCII "...".

Using "\xe2\x80\xa6" ensures getTextWidth measures one glyph instead of three, giving a more accurate width budget for the truncation loop and a typographically correct ellipsis.


856-862: Correct fix for the lost-currentLine last-line bug.

Combining currentLine + " " + remaining before delegating to truncatedText ensures content already accumulated on the in-progress line isn't silently discarded when the line-limit is hit mid-word.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/themes/lyra/Lyra3CoversTheme.cpp (1)

25-25: ⚠️ Potential issue | 🟡 Minor

Remove unused variable tileHeight (flagged by CI).

The pipeline's cppcheck reports that tileHeight is assigned but never read. This should be removed to keep the build warning-free.

Proposed fix
   const int tileWidth = (rect.width - 2 * Lyra3CoversMetrics::values.contentSidePadding) / 3;
-  const int tileHeight = rect.height;
   const int tileY = rect.y;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/themes/lyra/Lyra3CoversTheme.cpp` at line 25, Remove the
unused local variable tileHeight (the line "const int tileHeight =
rect.height;") which is assigned but never read; delete that declaration and, if
any later code intended to use tileHeight, replace those uses with rect.height
directly (search for "tileHeight" references) so no unused-variable warnings
remain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/GfxRenderer/GfxRenderer.cpp`:
- Around line 846-904: In wrappedText, when a carried-over word is too wide and
you call truncatedText (the branch that sets currentLine = truncatedText(...)
inside wrappedText), push that truncated string into lines immediately instead
of storing it in currentLine so you don't allow a short following word to be
appended after an ellipsis; after pushing the truncated entry, clear currentLine
(or set it to empty) so subsequent words start a new line, and add a check
against maxLines (using lines.size() and maxLines) to handle the case where
pushing the truncated line reaches the limit (mirror the existing early-return
behavior that uses truncatedText for the final allowed line). Ensure references:
wrappedText, currentLine, lines, truncatedText, getTextWidth, and maxLines.

---

Outside diff comments:
In `@src/components/themes/lyra/Lyra3CoversTheme.cpp`:
- Line 25: Remove the unused local variable tileHeight (the line "const int
tileHeight = rect.height;") which is assigned but never read; delete that
declaration and, if any later code intended to use tileHeight, replace those
uses with rect.height directly (search for "tileHeight" references) so no
unused-variable warnings remain.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3424c06 and f52f459.

📒 Files selected for processing (3)
  • lib/GfxRenderer/GfxRenderer.cpp
  • src/components/themes/BaseTheme.cpp
  • src/components/themes/lyra/Lyra3CoversTheme.cpp
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-02-19T12:17:05.421Z
Learnt from: Uri-Tauber
Repo: crosspoint-reader/crosspoint-reader PR: 988
File: lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp:649-661
Timestamp: 2026-02-19T12:17:05.421Z
Learning: In ChapterHtmlSlimParser.cpp, when computing footnote word indices in endElement() for footnote links, the wordIndex must be cumulative across the 750-word mid-paragraph flush boundary. The correct calculation is: `wordIndex = wordsExtractedInBlock + currentTextBlock->size()`, not just `currentTextBlock->size()`. This ensures footnotes attach to the page containing their actual anchor word, even after layoutAndExtractLines(false) has extracted and removed earlier words from the block.

Applied to files:

  • lib/GfxRenderer/GfxRenderer.cpp
🪛 GitHub Actions: CI (build)
src/components/themes/lyra/Lyra3CoversTheme.cpp

[warning] 25-25: cppcheck: Variable 'tileHeight' is assigned a value that is never used. [unreadVariable].

🔇 Additional comments (3)
lib/GfxRenderer/GfxRenderer.cpp (1)

831-832: Good change: UTF-8 ellipsis character.

Switching from "..." (3 ASCII dots) to U+2026 ("\xe2\x80\xa6") is the correct typographic choice for an e-reader UI and saves horizontal space since it occupies a single glyph advance.

src/components/themes/BaseTheme.cpp (1)

490-541: Clean refactoring to use wrappedText and truncatedText.

The replacement of manual wrapping logic with renderer.wrappedText() for the title and renderer.truncatedText() for the author is well-structured. The truncatedAuthor variable is correctly hoisted and reused for both width calculation and rendering, avoiding redundant calls.

src/components/themes/lyra/Lyra3CoversTheme.cpp (1)

89-116: Well-integrated use of wrappedText with dynamic title box height.

The dynamic height calculation based on titleLines.size() is a nice improvement over a fixed title area. The per-line rendering loop and padding logic look correct.

…Height

- wrappedText: when a carried-over word exceeds maxWidth, push its truncated
  form directly as a complete line instead of storing it in currentLine.
  Previously a subsequent short word could be appended after the ellipsis.
  Also returns early if the push reaches maxLines.
- Lyra3CoversTheme: remove unused tileHeight variable (compiler warning)
/// Word-wrap \p text into at most \p maxLines lines, each no wider than
/// \p maxWidth pixels. Overflowing words and excess lines are UTF-8-safely
/// truncated with an ellipsis (U+2026).
std::vector<std::string> wrappedText(int fontId, const char* text, int maxWidth, int maxLines,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits, but should we have a drawWrappedText() wrapper for this? the idea is like drawCenteredText(), that will simply downstream code

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, is it possible to also return a bool to indicate if the text is truncated? something like bool& truncated

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do that, let me see

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ngxson just getting around to looking, what do you mean by a wrapper?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean the wrappedText only tell how to wrap the text, but not drawing on screen. A wrapper like drawWrappedText will call wrappedText under the hood, and also draw the text to screen

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(just optional though because I will need that for another PR)

CaptainFrito
CaptainFrito previously approved these changes Feb 24, 2026
Copy link
Contributor

@CaptainFrito CaptainFrito left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you so much for this!!

@CaptainFrito
Copy link
Contributor

Very cool device by the way

ngxson
ngxson previously approved these changes Feb 24, 2026
@iandchasse iandchasse dismissed stale reviews from ngxson and CaptainFrito via 991828a February 24, 2026 12:22
Copy link
Contributor

@znelson znelson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Thanks for consolidating the logic 🙂

@ngxson ngxson merged commit 35988ad into crosspoint-reader:master Feb 25, 2026
6 checks passed
@iandchasse iandchasse deleted the refactor/gfx-wrapped-text branch February 25, 2026 12:00
el pushed a commit to el/crosspoint-reader that referenced this pull request Feb 26, 2026
…oint-reader#1141)

## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)


* **What changes are included?**
Conrgegate the changes of crosspoint-reader#1074 , crosspoint-reader#1013 , and extended upon crosspoint-reader#911 by
@lkristensen
New function implemented in GfxRenderer.cpp
```C++
std::vector<std::string> GfxRenderer::wrappedText(const int fontId, const char* text, const int maxWidth,
                                                  const int maxLines, const EpdFontFamily::Style style) const
```
Applied logic to all uses in Lyra, Lyra Extended, and base theme
(continue reading card as pointed out by @znelson


## Additional Context





![IMG_8604](https://github.com/user-attachments/assets/49da71c9-a44f-4cde-b3bf-6773d71601b6)

![IMG_8605](https://github.com/user-attachments/assets/5eab4293-65c1-47fb-b422-8ab53a6b50a2)

![IMG_8606](https://github.com/user-attachments/assets/e0f98d19-0e3f-4294-83a1-e49264378dca)


---

### 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 >**_

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants