Skip to content

feat: Lyra Icons#725

Merged
daveallie merged 18 commits intocrosspoint-reader:masterfrom
CaptainFrito:feature/lyra-icons
Feb 19, 2026
Merged

feat: Lyra Icons#725
daveallie merged 18 commits intocrosspoint-reader:masterfrom
CaptainFrito:feature/lyra-icons

Conversation

@CaptainFrito
Copy link
Contributor

@CaptainFrito CaptainFrito commented Feb 6, 2026

/!\ This PR depends on #732 being merged first

Also requires the open-x4-epaper/community-sdk#18 PR

Summary

Lyra theme icons on the home menu, in the file browser and on empty book covers

IMG_8023 Medium
IMG_8024 Medium
IMG_7958 Medium
IMG_8033 Medium

Additional Context

  • Added a function to the open-x4-sdk renderer to draw transparent images
  • Added a scripts/convert_icon.py script to convert svg/png icons into a C array that can be directly imported into the project. Usage:
python ./scripts/convert_icon.py 'path/to/icon.png' cover 32 32

This will create a components/icons/cover.h file with a C array called CoverIcon, of size 32x32px. Lyra uses icons from https://lucide.dev/icons with a stroke width of 2px, that can be downloaded with any desired size on the site.

The file browser is noticeably slower with the addition of icons, and using an image buffer like on the home page doesn't help very much. Any suggestions to optimize this are welcome.


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? PARTIALLY
The icon conversion python script was generated by Copilot as I am not a python dev.

@osteotek
Copy link
Member

osteotek commented Feb 6, 2026

The main menu item "OPDS Browser" doesn't fit its container now that there is an icon. What else can we name it?

can we use single line for each item, like in classic theme? Seems cramped now

@yagofarias
Copy link

yagofarias commented Feb 6, 2026

@osteotek just simulated how it would feels like to have a list item instead of mosaic. we could have it on settings to be changed. Same for bookshelf.
Item Arrangement <List/Mosaic>
Bookshelf Arrangement <Single/Multiple>
image

@lukestein
Copy link
Contributor

@osteotek just simulated how it would feels like to have a list item instead of mosaic. we could have it on settings to be changed. Same for bookshelf.

Item Arrangement <List/Mosaic>

Bookshelf Arrangement <Single/Multiple>

I think both of these look great, personally. But I'm unsure how much we want to go in the direction of having theme-specific settings for customization?

@yagofarias
Copy link

@lukestein Agreed.
Maybe for bookshelf if have just one book loaded it fills the section as single item, and if you have more it turns into multiple items.
For item/application list we could have a poll at discord to understand which one is preferred

@lepislepis
Copy link

lepislepis commented Feb 6, 2026

@lukestein Agreed. Maybe for bookshelf if have just one book loaded it fills the section as single item, and if you have more it turns into multiple items. For item/application list we could have a poll at discord to understand which one is preferred

I think the bookshelf could offer both single and multiple-item options. Personally, even if I have many books, I only read one at a time.

Also, using two lines of text instead of one improves book title visibility in the current build.

@CaptainFrito
Copy link
Contributor Author

CaptainFrito commented Feb 7, 2026

We could also offer both options through two separate themes. I personally really like the visual richness of 3 recent books on the home page, and it was too much of a PITA to implement to just scrape it now 😅

@mrtnvgr
Copy link
Contributor

mrtnvgr commented Feb 7, 2026

Icons look cool! Single row layout of menu options fills empty space nicely :) I have a suggestion. "Calibre library" could be more broad, maybe something like "Remote library"?

@itsthisjustin
Copy link
Contributor

Icons look cool! Single row layout of menu options fills empty space nicely :) I have a suggestion. "Calibre library" could be more broad, maybe something like "Remote library"?

I dont know if maybe my PR just isn't in yet but I already renamed it to that haha

@itsthisjustin
Copy link
Contributor

fwiw horiztonal with icons looks SO much better

@CaptainFrito CaptainFrito force-pushed the feature/lyra-icons branch 4 times, most recently from d67c941 to 2c82317 Compare February 9, 2026 13:16
@CaptainFrito CaptainFrito force-pushed the feature/lyra-icons branch 2 times, most recently from 136e0e4 to 0347712 Compare February 9, 2026 15:24
daveallie pushed a commit to open-x4-epaper/community-sdk that referenced this pull request Feb 10, 2026
Adding a method to the display renderer to support drawing images
without coloring the white pixels, essentially drawing transparent
images for icons in the Lyra Icons PR
crosspoint-reader/crosspoint-reader#725
@CaptainFrito CaptainFrito force-pushed the feature/lyra-icons branch 2 times, most recently from 3ebdb27 to 7cf2dc2 Compare February 10, 2026 12:01
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 10, 2026

Caution

Review failed

The pull request is closed.

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d325a1e and 08722a8.

📒 Files selected for processing (1)
  • src/components/themes/lyra/Lyra3CoversTheme.cpp

📝 Walkthrough

Walkthrough

Adds an icon system: new transparent image draw call in HAL, many embedded icon headers, a UIIcon enum and file-type classifier, theme and activity APIs updated to accept UIIcon-based icon lambdas, Lyra theme icon rendering, and a script to convert images/SVGs to C bitmap headers.

Changes

Cohort / File(s) Summary
Display / HAL
lib/GfxRenderer/GfxRenderer.cpp, lib/hal/HalDisplay.cpp, lib/hal/HalDisplay.h
Switched icon draw call to drawImageTransparent; added HalDisplay::drawImageTransparent(...) that forwards to einkDisplay.drawImageTransparent(...).
Icon generation script
scripts/convert_icon.py
New CLI utility to convert SVG/raster to 1-bit C bitmap headers saved under src/components/icons/ (resize, threshold, pack bits).
Icon assets
src/components/icons/*
book.h, book24.h, cog.h, cover.h, folder.h, folder24.h, hotspot.h, image24.h, library.h, recent.h, text24.h, transfer.h, wifi.h, file24.h, settings.h, settings2.h
Added multiple 24/32px bitmap icon headers (static const uint8_t arrays) for use by themes and UI.
Theme API / BaseTheme
src/components/themes/BaseTheme.h, src/components/themes/BaseTheme.cpp
Added UIIcon enum; changed drawList/drawButtonMenu to accept std::function<UIIcon(int)> for row icons; added helpers (drawSubHeader, drawHelpText, drawTextField, drawKeyboardKey) and subtitle support in drawHeader.
Lyra theme
src/components/themes/lyra/LyraTheme.h, src/components/themes/lyra/LyraTheme.cpp
Updated signatures to use UIIcon icons and highlightValue; added per-row icon rendering (24/32px selection), drawSubHeader, icon mapping helper iconForName, and enhanced empty-cover placeholder rendering using CoverIcon.
Lyra3CoversTheme
src/components/themes/lyra/Lyra3CoversTheme.cpp
Adjusted recent-cover rendering to draw decorated empty cover and reference cover icon when missing.
UITheme / File classification
src/components/UITheme.h, src/components/UITheme.cpp
Added UITheme::getFileIcon(std::string) to map filenames/extensions to UIIcon; added LYRA_3_COVERS theme support and relevant forward decl/includes.
Activity updates
src/activities/home/..., src/activities/network/...
HomeActivity.cpp, MyLibraryActivity.cpp, RecentBooksActivity.cpp, NetworkModeSelectionActivity.cpp
Updated list/menu rendering to supply icon lambdas returning UIIcon; added getFileName helper and adjusted header/layout logic to use theme metrics and icons.
Theme includes & minor UI tweaks
src/components/themes/...
Multiple themes updated to include and render new icon assets, adjust line endpoints and layout to accommodate icons and subtitle/subheader rendering.

Sequence Diagram(s)

sequenceDiagram
    participant Activity as Activity (Home/Network/Recent)
    participant Theme as Theme (Lyra/Base)
    participant Renderer as GfxRenderer
    participant HAL as HalDisplay
    participant EInk as einkDisplay

    Activity->>Theme: request drawList / drawButtonMenu (rowTitle, rowSubtitle, rowIcon -> UIIcon)
    Theme->>Renderer: layout items, request icon bitmap for UIIcon
    Renderer->>HAL: drawImageTransparent(imageData, x, y, w, h, fromProgmem)
    HAL->>EInk: einkDisplay.drawImageTransparent(imageData, x, y, w, h, fromProgmem)
    EInk-->>HAL: rendered
    HAL-->>Renderer: ack
    Renderer-->>Theme: draw complete
    Theme-->>Activity: UI rendered
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • feat: Lyra screens #732 — Related changes around Lyra theme and icon/list rendering that touch UIIcon usage and theme/icon integration.

Suggested reviewers

  • lukestein
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Lyra Icons' directly and concisely describes the main change: adding icon support to the Lyra theme across multiple UI surfaces (home menu, file browser, empty book covers).
Description check ✅ Passed The description comprehensively relates to the changeset by documenting the feature implementation, dependencies, usage examples for the icon conversion script, performance concerns, and includes visual demonstration images.

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


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

@daveallie
Copy link
Member

oh uh, I didn't mean for this to run everywhere straight away

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: 12

Caution

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

⚠️ Outside diff range comments (3)
src/activities/network/WifiSelectionActivity.h (1)

47-103: ⚠️ Potential issue | 🟡 Minor

Align selectedNetworkIndex type with int-based APIs.

selectedNetworkIndex is declared as size_t but is passed to selectNetwork(int) and ButtonNavigator::nextIndex/previousIndex(int, int), which expect int. While the current code doesn't use negative values or sentinels (ButtonNavigator's modulo logic always returns valid non-negative results), this type mismatch creates implicit conversions at lines 449, 475, and 480, and misaligns with GUI.drawList expectations at line 585. Change selectedNetworkIndex to int for consistency.

src/components/UITheme.cpp (1)

28-45: ⚠️ Potential issue | 🟠 Major

Memory leak when switching themes.

When setTheme is called (e.g., via reload()), a new theme is allocated with new but the previous currentTheme pointer is not deleted, causing a memory leak each time the theme is changed.

🐛 Proposed fix
 void UITheme::setTheme(CrossPointSettings::UI_THEME type) {
+  delete currentTheme;
+  currentTheme = nullptr;
+
   switch (type) {
     case CrossPointSettings::UI_THEME::CLASSIC:
       Serial.printf("[%lu] [UI] Using Classic theme\n", millis());
       currentTheme = new BaseTheme();
src/activities/settings/OtaUpdateActivity.cpp (1)

231-233: ⚠️ Potential issue | 🟡 Minor

Remove unreachable SHUTTING_DOWN state handler—this code is never executed.

The SHUTTING_DOWN state is defined in the enum but never assigned anywhere in OtaUpdateActivity. State transitions flow from UPDATE_IN_PROGRESS directly to FAILED or FINISHED. The handler at lines 231–232 is dead code and should be removed.

🤖 Fix all issues with AI agents
In `@lib/GfxRenderer/GfxRenderer.cpp`:
- Around line 406-408: The drawIcon implementation passes parameters to
display.drawImageTransparent in the wrong order; in GfxRenderer::drawIcon swap
the x/y and width/height arguments so the call matches the API (imageData, x, y,
w, h). Replace the current call to display.drawImageTransparent(bitmap, y,
getScreenWidth() - width - x, height, width) with a call that supplies
getScreenWidth() - width - x as the x argument, y as the y argument, and width
then height as the final two args (i.e., imageData, x, y, w, h) so non-square
icons render correctly.

In `@scripts/convert_icon.py`:
- Around line 61-66: Update the usage string to reference convert_icon.py and
sanitize output_name before building array_name: replace or map non-alphanumeric
characters to underscores and ensure the identifier does not start with a digit
(prefix with an underscore if it does), then construct array_name from the
sanitized name (e.g., sanitized_name + 'Icon') instead of using capitalize();
keep the existing sys.argv unpacking (input_path, output_name, width, height)
and convert width/height to int as before.
- Around line 15-27: The SVG branch in load_image uses svg_to_png_bytes(path,
width, height) and Image.open on the resulting bytes but does not composite
transparent pixels onto white, causing transparent areas to become dark after
grayscale/threshold; modify the SVG branch so after opening the png bytes (img =
Image.open(io.BytesIO(png_bytes))) you convert to 'RGBA', resize if needed, then
composite/paste the image onto a white RGBA background (same technique as the
raster branch: create background = Image.new('RGBA', img.size,
(255,255,255,255')) and background.paste(img, mask=img.split()[3]) ) and assign
img = background before returning/processing further.

In `@src/activities/home/RecentBooksActivity.cpp`:
- Around line 140-145: The lambda that extracts extensions from recentBooks
(uses filename.rfind('.') and substr) must guard against no-extension cases and
dots in directory names: compute auto dot = filename.rfind('.'); if dot ==
std::string::npos then return empty string; also find the last path separator
with auto sep = filename.find_last_of("/\\"); only treat dot as an extension
when dot > sep; otherwise return empty string; call substr(dot + 1) only in the
valid branch so you never return partial/full paths.

In `@src/activities/network/CalibreConnectActivity.cpp`:
- Around line 218-264: The button hints are only drawn inside the SERVER_RUNNING
branch, leaving SERVER_STARTING and ERROR without navigation hints; move the
call that obtains labels via mappedInput.mapLabels("« Exit", "", "", "") and the
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3,
labels.btn4) out of the state-specific if/else block so it runs for all states
(or alternatively call GUI.drawButtonHints in both the SERVER_STARTING and ERROR
branches), ensuring CalibreConnectState::SERVER_STARTING and
CalibreConnectState::ERROR also render the exit hint.

In `@src/activities/settings/OtaUpdateActivity.cpp`:
- Around line 166-169: The NO_UPDATE and FAILED render branches currently call
renderer.drawCenteredText but omit the Back button hints; update those branches
(state == NO_UPDATE and state == FAILED) to also draw the same Back button hint
used elsewhere in this activity (use the same renderer button-hint call that
other states use) so users see they can exit—keep the existing centered text and
add the button-hint drawing with the same styling and placement as other states;
no changes to loop() handler are needed.

In `@src/components/themes/BaseTheme.cpp`:
- Line 20: Replace the hard-coded subtitleY constant with a computed value
derived from the header rectangle so subtitles follow changes in header height
or screen size: locate the usage of subtitleY in BaseTheme (and the similar code
around the other occurrences at lines referenced 252-258), remove constexpr int
subtitleY = 738 and compute subtitleY at runtime from the header rect (e.g., use
headerRect.bottom or headerRect.y + headerRect.height plus an optional margin)
wherever subtitleY is used (e.g., in layout/subtitle drawing functions) so the
subtitle position is always based on headerRect properties instead of a fixed
coordinate.
- Line 67: The Serial.printf call in the progress-bar rendering path should be
guarded and use the correct size_t formatting: replace the unguarded
Serial.printf("Drawing progress bar: current=%u, total=%u, percent=%d\n",
current, total, percent); with a conditional log (either a runtime verbose/debug
flag check or a compile-time `#ifdef` DEBUG/LOG_ENABLED) so it does not run in the
hot render path, and change the format specifiers for current and total to %zu
(or cast to an explicit integer type and use %lu if your platform lacks %zu)
while keeping percent as %d; locate the call by searching for Serial.printf in
the progress-bar drawing function and wrap it accordingly.

In `@src/components/themes/lyra/Lyra3CoversTheme.cpp`:
- Around line 70-78: The empty-cover rendering block inside Lyra3CoversTheme.cpp
uses LyraMetrics::values.homeCoverHeight causing inconsistency with the rest of
this theme; update the three occurrences used in the hasCover == false branch
(the calls to renderer.drawRect and renderer.fillRect that reference
homeCoverHeight, and any related calculations in that block) to use
Lyra3CoversMetrics::values.homeCoverHeight so the placeholder height matches the
Lyra3Covers theme metrics.

In `@src/components/themes/lyra/Lyra3CoversTheme.h`:
- Around line 38-40: The declaration of Lyra3CoversTheme::drawRecentBookCover
has a mismatched parameter constness (GfxRenderer& renderer) so it does not
override the base virtual; change the first parameter to const GfxRenderer&
renderer to match LyraTheme::drawRecentBookCover and update any corresponding
definition/implementation signatures (e.g., the drawRecentBookCover method in
Lyra3CoversTheme.cpp) so the function properly overrides with the const
qualifier and keeps the existing override specifier.

In `@src/components/themes/lyra/LyraTheme.cpp`:
- Around line 359-405: The code currently calls storeCoverBuffer() on every
render which copies the framebuffer; change it so storeCoverBuffer() is only
invoked when the cover has just been rendered or when the cover source changes:
call storeCoverBuffer() inside the branch where coverRendered is transitioned to
true (i.e., immediately after successfully drawing the bitmap or rendering the
empty cover) and only if the coverBufferStored flag is not already set or the
current RecentBook cover path (book.coverBmpPath / coverPath) differs from the
previously stored cover path; update coverBufferStored and remember the stored
cover path when you call storeCoverBuffer(), and remove the unconditional call
to storeCoverBuffer() that currently executes every render.
- Around line 449-459: The code uses a dangling pointer from
buttonLabel(i).c_str() and ignores the rowIcon parameter; fix by storing the
label in a local std::string (e.g., std::string label = buttonLabel(i);) so its
.c_str() remains valid for later text rendering, and use rowIcon(i) when
resolving the icon (e.g., const char* iconName = rowIcon(i); if (iconName !=
nullptr) { const uint8_t* icon = iconForName(iconName, mainMenuIconSize); ...
}). Keep the existing null checks and only advance textX when an icon was
actually drawn.
🧹 Nitpick comments (10)
src/activities/home/MyLibraryActivity.cpp (1)

227-241: Pass parameters by const& and handle missing extensions.

  1. Copy overhead: Both methods take std::string by value, causing unnecessary allocations on each call. Use const std::string& instead.

  2. Edge case: For files without an extension (e.g., README, Makefile), rfind('.') returns npos. In getFileType, substr(npos + 1) wraps to substr(0), returning the entire filename as the type. In getFileName, it returns the full name (acceptable but inconsistent).

♻️ Proposed fix
-std::string MyLibraryActivity::getFileType(std::string filename) const {
+std::string MyLibraryActivity::getFileType(const std::string& filename) const {
   if (filename.back() == '/') {
     return "Folder";
   }
   const auto pos = filename.rfind('.');
+  if (pos == std::string::npos) {
+    return "File";
+  }
   return filename.substr(pos + 1);
 }

-std::string MyLibraryActivity::getFileName(std::string filename) const {
+std::string MyLibraryActivity::getFileName(const std::string& filename) const {
   if (filename.back() == '/') {
     return filename.substr(0, filename.length() - 1);
   }
   const auto pos = filename.rfind('.');
+  if (pos == std::string::npos) {
+    return filename;
+  }
   return filename.substr(0, pos);
 }
src/activities/home/MyLibraryActivity.h (1)

38-39: Update declarations to use const std::string&.

Consistent with the implementation fix suggested in MyLibraryActivity.cpp, update these declarations to avoid pass-by-value copies.

♻️ Proposed fix
-  std::string getFileType(std::string filename) const;
-  std::string getFileName(std::string filename) const;
+  std::string getFileType(const std::string& filename) const;
+  std::string getFileName(const std::string& filename) const;
src/activities/network/NetworkModeSelectionActivity.cpp (1)

10-21: Consider deriving item count from the arrays to prevent drift.

MENU_ITEM_COUNT can diverge from the actual array sizes as items evolve. Using std::array and size() avoids this risk.

♻️ Suggested refactor
+#include <array>
 namespace {
-constexpr int MENU_ITEM_COUNT = 3;
-const char* MENU_ITEMS[MENU_ITEM_COUNT] = {"Join a Network", "Connect to Calibre", "Create Hotspot"};
-const char* MENU_DESCRIPTIONS[MENU_ITEM_COUNT] = {
+constexpr std::array<const char*, 3> MENU_ITEMS = {
+    "Join a Network", "Connect to Calibre", "Create Hotspot"};
+constexpr std::array<const char*, 3> MENU_DESCRIPTIONS = {
     "Connect to an existing WiFi network",
     "Use Calibre wireless device transfers",
     "Create a WiFi network others can join",
 };
-const char* MENU_ICONS[MENU_ITEM_COUNT] = {
+constexpr std::array<const char*, 3> MENU_ICONS = {
     "wifi",
-    "OPDS Browser",
+    "OPDS Browser",
     "hotspot",
 };
+constexpr int MENU_ITEM_COUNT = static_cast<int>(MENU_ITEMS.size());
 }  // namespace
src/activities/network/CrossPointWebServerActivity.cpp (1)

431-494: Avoid double header/subheader rendering.
render() already draws the header/subheader for SERVER_RUNNING; doing it again here adds extra draw work with no visual benefit. If renderServerRunning() is only called from render(), you can drop these calls.

♻️ Suggested trim
-  GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight},
-                 isApMode ? "Hotspot Mode" : "Network Transfer", nullptr);
-  GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
-                    connectedSSID.c_str());
src/components/icons/library.h (1)

1-12: Avoid per‑TU duplication of icon data.

static in a header produces one copy per translation unit. For embedded builds this can add up across many icons. If C++17 is available, prefer inline constexpr to keep a single ODR entity, or move the definition to a .cpp with an extern declaration.

💡 Example (C++17 inline variable)
- static const uint8_t LibraryIcon[] = {
+ inline constexpr uint8_t LibraryIcon[] = {
src/components/themes/lyra/LyraTheme.cpp (2)

47-63: Normalize icon names once to avoid case drift and brittle matches.

A single lowercase normalization makes icon lookups resilient to label casing changes and keeps comparisons consistent.

🛠️ Proposed refactor
@@
-#include <algorithm>
+#include <algorithm>
+#include <cctype>
@@
 const uint8_t* iconForName(const std::string& iconName, int size) {
+  std::string key = iconName;
+  std::transform(key.begin(), key.end(), key.begin(),
+                 [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
   if (size == 24) {
-    if (iconName == "Folder") return Folder24Icon;
-    if (iconName == "epub" || iconName == "xtc" || iconName == "xtch") return Book24Icon;
-    if (iconName == "txt") return Text24Icon;
-    if (iconName == "bmp") return Image24Icon;
+    if (key == "folder") return Folder24Icon;
+    if (key == "epub" || key == "xtc" || key == "xtch") return Book24Icon;
+    if (key == "txt") return Text24Icon;
+    if (key == "bmp") return Image24Icon;
   } else if (size == 32) {
-    if (iconName == "epub" || iconName == "xtc" || iconName == "xtch") return BookIcon;
-    if (iconName == "Browse Files") return FolderIcon;
-    if (iconName == "Recents") return RecentIcon;
-    if (iconName == "Settings") return CogIcon;
-    if (iconName == "File Transfer") return TransferIcon;
-    if (iconName == "OPDS Browser") return LibraryIcon;
-    if (iconName == "wifi") return WifiIcon;
-    if (iconName == "hotspot") return HotspotIcon;
+    if (key == "epub" || key == "xtc" || key == "xtch") return BookIcon;
+    if (key == "browse files") return FolderIcon;
+    if (key == "recents") return RecentIcon;
+    if (key == "settings") return CogIcon;
+    if (key == "file transfer") return TransferIcon;
+    if (key == "opds browser") return LibraryIcon;
+    if (key == "wifi") return WifiIcon;
+    if (key == "hotspot") return HotspotIcon;
   }
   return nullptr;
 }

223-279: Consider caching icon/value text for large lists to reduce per-frame allocations.

Each redraw allocates multiple std::strings and performs icon lookups per row, which can contribute to the reported file-browser slowdown. Caching resolved icon pointers or switching callbacks to return std::string_view/const char* (or memoized strings) would reduce heap churn.

src/activities/settings/KOReaderAuthActivity.cpp (1)

126-148: Consider using metrics for vertical spacing between text lines.

The hardcoded + 10 spacing between lines (lines 140, 143) could be replaced with metrics.verticalSpacing for consistency with the metrics-driven approach used elsewhere in the render function.

♻️ Suggested improvement
+  auto metrics = UITheme::getInstance().getMetrics();
   const auto pageWidth = renderer.getScreenWidth();
   const auto pageHeight = renderer.getScreenHeight();
   ...
   if (state == SUCCESS) {
     renderer.drawCenteredText(UI_10_FONT_ID, top, "Success!", true, EpdFontFamily::BOLD);
-    renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, "KOReader sync is ready to use");
+    renderer.drawCenteredText(UI_10_FONT_ID, top + height + metrics.verticalSpacing, "KOReader sync is ready to use");
   } else if (state == FAILED) {
     renderer.drawCenteredText(UI_10_FONT_ID, top, "Error", true, EpdFontFamily::BOLD);
-    renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, errorMessage.c_str());
+    renderer.drawCenteredText(UI_10_FONT_ID, top + height + metrics.verticalSpacing, errorMessage.c_str());
   }
src/components/themes/lyra/Lyra3CoversTheme.cpp (1)

17-21: Unused mutable global coverWidth in anonymous namespace.

The coverWidth variable at line 20 is declared but never used in this file. The local variable coverWidth at line 54 shadows it. This appears to be dead code copied from LyraTheme.cpp where it serves a caching purpose.

♻️ Proposed fix
 namespace {
 constexpr int hPaddingInSelection = 8;
 constexpr int cornerRadius = 6;
-int coverWidth = 0;
 }  // namespace
src/activities/settings/OtaUpdateActivity.cpp (1)

121-137: Early return after clearScreen() wastes work.

When the progress throttling condition triggers (line 134), the function returns after clearScreen() and drawHeader() have already executed. While this doesn't cause visual issues (since displayBuffer() isn't called), these operations are performed unnecessarily.

♻️ Move throttling check before drawing operations
 void OtaUpdateActivity::render() {
   if (subActivity) {
     // Subactivity handles its own rendering
     return;
   }

+  // Throttle progress updates early to avoid unnecessary draw operations
+  if (state == UPDATE_IN_PROGRESS) {
+    float progress = static_cast<float>(updater.getProcessedSize()) / static_cast<float>(updater.getTotalSize());
+    if (static_cast<int>(progress * 50) == lastUpdaterPercentage / 2) {
+      return;
+    }
+    lastUpdaterPercentage = static_cast<int>(progress * 100);
+  }
+
   auto metrics = UITheme::getInstance().getMetrics();
   const auto pageWidth = renderer.getScreenWidth();
   const auto pageHeight = renderer.getScreenHeight();

   renderer.clearScreen();

   GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "Update");
   const auto height = renderer.getLineHeight(UI_10_FONT_ID);
   const auto top = (pageHeight - height) / 2;

-  float updaterProgress = 0;
-  if (state == UPDATE_IN_PROGRESS) {
-    Serial.printf("[%lu] [OTA] Update progress: %d / %d\n", millis(), updater.getProcessedSize(),
-                  updater.getTotalSize());
-    updaterProgress = static_cast<float>(updater.getProcessedSize()) / static_cast<float>(updater.getTotalSize());
-    // Only update every 2% at the most
-    if (static_cast<int>(updaterProgress * 50) == lastUpdaterPercentage / 2) {
-      return;
-    }
-    lastUpdaterPercentage = static_cast<int>(updaterProgress * 100);
-  }
+  float updaterProgress = (state == UPDATE_IN_PROGRESS)
+      ? static_cast<float>(updater.getProcessedSize()) / static_cast<float>(updater.getTotalSize())
+      : 0;

Comment on lines +449 to +459
int textX = tileRect.x + 16;
const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
const int textY = tileRect.y + (LyraMetrics::values.menuRowHeight - lineHeight) / 2;
const char* label = buttonLabel(i).c_str();

if (rowIcon != nullptr) {
const uint8_t* icon = iconForName(label, mainMenuIconSize);
if (icon != nullptr) {
renderer.drawIcon(icon, textX, textY, mainMenuIconSize, mainMenuIconSize);
textX += mainMenuIconSize + hPaddingInSelection;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the file and read the code around lines 449-459
cat -n src/components/themes/lyra/LyraTheme.cpp | sed -n '440,470p'

Repository: crosspoint-reader/crosspoint-reader

Length of output: 1557


🏁 Script executed:

# Search for buttonLabel function definition
rg -A 5 "buttonLabel\s*\(" src/components/themes/lyra/LyraTheme.cpp | head -20

Repository: crosspoint-reader/crosspoint-reader

Length of output: 335


🏁 Script executed:

# Find the header file for LyraTheme
fd "LyraTheme" src/components/themes/lyra/

Repository: crosspoint-reader/crosspoint-reader

Length of output: 158


🏁 Script executed:

# Check the header file for class definition and method signatures
cat -n src/components/themes/lyra/LyraTheme.h

Repository: crosspoint-reader/crosspoint-reader

Length of output: 3855


🏁 Script executed:

# Let's see the broader context of the drawButtonMenu function
cat -n src/components/themes/lyra/LyraTheme.cpp | sed -n '420,465p'

Repository: crosspoint-reader/crosspoint-reader

Length of output: 2650


🏁 Script executed:

# Let's check the iconForName function to understand what it expects
rg -B 3 -A 10 "iconForName" src/components/themes/lyra/LyraTheme.cpp | head -30

Repository: crosspoint-reader/crosspoint-reader

Length of output: 1264


🏁 Script executed:

# Let's see more of the iconForName function and confirm the signature
rg -B 2 -A 20 "const uint8_t\* iconForName" src/components/themes/lyra/LyraTheme.cpp | head -40

Repository: crosspoint-reader/crosspoint-reader

Length of output: 1026


🏁 Script executed:

# Check if label is used anywhere else after line 462 in drawButtonMenu
cat -n src/components/themes/lyra/LyraTheme.cpp | sed -n '462,475p'

Repository: crosspoint-reader/crosspoint-reader

Length of output: 737


Fix dangling label pointer and honor rowIcon parameter.

buttonLabel(i).c_str() returns a pointer to a temporary std::string that is destroyed at statement end, leaving a dangling pointer used by renderer.drawText() on line 462. Additionally, the icon lookup uses the button label instead of calling rowIcon(i), which makes icons brittle to label changes and ignores the parameter entirely.

🛠️ Proposed fix
@@
-    const char* label = buttonLabel(i).c_str();
+    std::string labelStr = buttonLabel(i);
+    const char* label = labelStr.c_str();
@@
-    if (rowIcon != nullptr) {
-      const uint8_t* icon = iconForName(label, mainMenuIconSize);
+    if (rowIcon != nullptr) {
+      std::string iconKey = rowIcon(i);
+      const uint8_t* icon = iconForName(iconKey, mainMenuIconSize);
       if (icon != nullptr) {
         renderer.drawIcon(icon, textX, textY, mainMenuIconSize, mainMenuIconSize);
         textX += mainMenuIconSize + hPaddingInSelection;
       }
     }
🤖 Prompt for AI Agents
In `@src/components/themes/lyra/LyraTheme.cpp` around lines 449 - 459, The code
uses a dangling pointer from buttonLabel(i).c_str() and ignores the rowIcon
parameter; fix by storing the label in a local std::string (e.g., std::string
label = buttonLabel(i);) so its .c_str() remains valid for later text rendering,
and use rowIcon(i) when resolving the icon (e.g., const char* iconName =
rowIcon(i); if (iconName != nullptr) { const uint8_t* icon =
iconForName(iconName, mainMenuIconSize); ... }). Keep the existing null checks
and only advance textX when an icon was actually drawn.

@CaptainFrito CaptainFrito force-pushed the feature/lyra-icons branch 6 times, most recently from 25ade24 to 12f3902 Compare February 12, 2026 07:04
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: 4

Caution

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

⚠️ Outside diff range comments (1)
src/activities/settings/OtaUpdateActivity.cpp (1)

99-105: ⚠️ Potential issue | 🟠 Major

Guard against zero getTotalSize() before division.

Line 100 divides by updater.getTotalSize(). If the total is 0 (unknown length or early stage), this can yield NaN/inf and lead to undefined behavior when cast to int later in the render path. Add a zero/negative guard.

🛠️ Suggested fix
-  updaterProgress = static_cast<float>(updater.getProcessedSize()) / static_cast<float>(updater.getTotalSize());
+  const auto totalSize = updater.getTotalSize();
+  const auto processedSize = updater.getProcessedSize();
+  if (totalSize <= 0) {
+    updaterProgress = 0.0f;
+  } else {
+    updaterProgress = static_cast<float>(processedSize) / static_cast<float>(totalSize);
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/activities/settings/OtaUpdateActivity.cpp` around lines 99 - 105, The
division by updater.getTotalSize() in OtaUpdateActivity.cpp can divide by zero;
modify the updater progress calculation (where updaterProgress,
updater.getProcessedSize(), updater.getTotalSize(), and lastUpdaterPercentage
are used) to first check that updater.getTotalSize() > 0 (or >= 1) and only
compute the ratio when positive; if total is zero or negative, set
updaterProgress to 0 (or an appropriate sentinel) and skip the percentage-based
early return/update logic so subsequent casts to int and comparisons cannot use
NaN/inf.
🧹 Nitpick comments (14)
lib/GfxRenderer/GfxRenderer.cpp (1)

323-325: Clarify the heuristic assumption in the comment.

The comment says "Assume if we're not rounding all corners then we are only rounding one side," but the condition triggers when any corner is not rounded. This assumption holds for common cases (e.g., rounding only top or only left side) but could produce unexpected results if:

  • Diagonal corners are rounded (top-left + bottom-right)
  • Three corners are rounded

If these scenarios are intentionally unsupported, consider documenting that constraint. Otherwise, this is a reasonable pragmatic heuristic.

📝 Suggested comment clarification
-  // Assume if we're not rounding all corners then we are only rounding one side
+  // When partially rounding corners, assume we're rounding one side (top, bottom, left, or right).
+  // Diagonal or three-corner rounding may produce suboptimal radius clamping.
   const int roundedSides = (!roundTopLeft || !roundTopRight || !roundBottomLeft || !roundBottomRight) ? 1 : 2;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/GfxRenderer/GfxRenderer.cpp` around lines 323 - 325, The current comment
misstates the condition used to compute roundedSides: the code sets roundedSides
based on (!roundTopLeft || !roundTopRight || !roundBottomLeft ||
!roundBottomRight) which triggers when any corner is not rounded, so update the
comment to clearly describe this heuristic and its limitations (it assumes
either all corners or effectively one side is rounded and may produce odd
results for diagonal or three-corner cases) and either (A) document that
diagonal/three-corner cases are unsupported, or (B) replace the heuristic by
computing an explicit roundedCount (e.g., count true corner flags) and use that
to derive roundedSides/maxRadius to handle mixed cases; reference the
roundedSides, cornerRadius and maxRadius symbols when making the change.
src/activities/settings/OtaUpdateActivity.cpp (1)

87-105: Avoid doing full render work when throttling progress updates.

When the 2% throttle triggers (Line 102), the function returns after clearScreen() and header draw, doing work that never reaches displayBuffer(). Consider moving the throttle check earlier (before clearing/drawing) to save CPU/battery.

♻️ Possible refactor
-  renderer.clearScreen();
-
-  GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_UPDATE));
-  const auto height = renderer.getLineHeight(UI_10_FONT_ID);
-  const auto top = (pageHeight - height) / 2;
-
   float updaterProgress = 0;
   if (state == UPDATE_IN_PROGRESS) {
     LOG_DBG("OTA", "Update progress: %d / %d", updater.getProcessedSize(), updater.getTotalSize());
     updaterProgress = static_cast<float>(updater.getProcessedSize()) / static_cast<float>(updater.getTotalSize());
     // Only update every 2% at the most
     if (static_cast<int>(updaterProgress * 50) == lastUpdaterPercentage / 2) {
       return;
     }
     lastUpdaterPercentage = static_cast<int>(updaterProgress * 100);
   }
+
+  renderer.clearScreen();
+  GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_UPDATE));
+  const auto height = renderer.getLineHeight(UI_10_FONT_ID);
+  const auto top = (pageHeight - height) / 2;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/activities/settings/OtaUpdateActivity.cpp` around lines 87 - 105, Compute
updaterProgress and perform the 2% throttle check before doing any heavy
rendering (i.e., before calling renderer.clearScreen() and GUI.drawHeader()).
Specifically, call updater.getProcessedSize() and updater.getTotalSize(),
compute updaterProgress, compare the throttled percentage against
lastUpdaterPercentage, and return early if unchanged; only if the check passes
proceed to clear the screen, draw the header and continue updating
lastUpdaterPercentage. Update references to updaterProgress,
lastUpdaterPercentage, renderer.clearScreen(), GUI.drawHeader(),
updater.getProcessedSize(), and updater.getTotalSize() accordingly so no
unnecessary work is done when throttled.
src/components/UITheme.h (2)

26-26: Consider passing filename by const reference to avoid unnecessary copy.

The PR mentions the file browser is noticeably slower after adding icons. Passing std::string by value creates an unnecessary copy on each call. Since getFileIcon only reads the string, passing by const std::string& would be more efficient.

♻️ Proposed fix
-  static UIIcon getFileIcon(std::string filename);
+  static UIIcon getFileIcon(const std::string& filename);

The implementation in UITheme.cpp should also be updated to match.

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

In `@src/components/UITheme.h` at line 26, Change the getFileIcon API to accept
the filename by const reference to avoid copies: update the declaration static
UIIcon getFileIcon(const std::string& filename) in UITheme.h and update the
corresponding definition in UITheme.cpp to match (function name getFileIcon).
Also update any call sites that relied on the by-value signature (pass
std::string or const std::string& as before) to ensure they compile. Rebuild and
run the file browser performance regression test to confirm improvement.

9-10: Unused forward declaration.

MappedInputManager is forward-declared here but doesn't appear to be used anywhere in this header file. If it's only needed in the implementation file (UITheme.cpp), consider moving the forward declaration or include there instead.

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

In `@src/components/UITheme.h` around lines 9 - 10, Remove the unused forward
declaration of MappedInputManager from UITheme.h; if the type is only referenced
in UITheme.cpp, move the forward declaration or the `#include` for
MappedInputManager into UITheme.cpp (or include the header where it is actually
used) so UITheme.h no longer contains an unused forward declaration.
src/activities/settings/ButtonRemapActivity.cpp (1)

136-141: Help text positioning is tightly coupled to list row count.

The calculations 4 * metrics.listRowHeight and 5 * metrics.verticalSpacing assume exactly 4 list items. While this matches kRoleCount, if the role count changes, these magic multipliers would need manual updates. Consider deriving the position from the list's computed bounds or using kRoleCount in the calculation.

♻️ Proposed improvement
   // Provide side button actions at the bottom of the screen (split across two lines).
+  const int helpTextTop = topOffset + kRoleCount * metrics.listRowHeight + kRoleCount * metrics.verticalSpacing;
   GUI.drawHelpText(renderer,
-                   Rect{0, topOffset + 4 * metrics.listRowHeight + 4 * metrics.verticalSpacing, pageWidth, 20},
+                   Rect{0, helpTextTop, pageWidth, 20},
                    tr(STR_REMAP_RESET_HINT));
   GUI.drawHelpText(renderer,
-                   Rect{0, topOffset + 4 * metrics.listRowHeight + 5 * metrics.verticalSpacing + 20, pageWidth, 20},
+                   Rect{0, helpTextTop + metrics.verticalSpacing + 20, pageWidth, 20},
                    tr(STR_REMAP_CANCEL_HINT));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/activities/settings/ButtonRemapActivity.cpp` around lines 136 - 141, The
help text Y offsets are hard-coded with “4 * metrics.listRowHeight” and “5 *
metrics.verticalSpacing” which assumes 4 list items; update the calculation to
derive the vertical position from the actual list bounds or the canonical role
count instead of magic numbers: use the list's computed bounds (e.g. the
variable that holds the list Rect or call that computes its height) or replace
the literal multipliers with kRoleCount * metrics.listRowHeight and kRoleCount *
metrics.verticalSpacing so GUI.drawHelpText calls compute topOffset +
listBounds.height (or topOffset + kRoleCount * metrics.listRowHeight) +
additional spacing for the two help lines, referencing GUI.drawHelpText,
metrics.listRowHeight, metrics.verticalSpacing, and kRoleCount (or the list
bounds variable) to locate and update the code.
src/components/themes/lyra/Lyra3CoversTheme.h (1)

1-5: Minor: Remove leading blank lines.

The file starts with two blank lines before #pragma once. This is a minor style inconsistency.

♻️ Proposed fix
-
-
 `#pragma` once

 `#include` "components/themes/lyra/LyraTheme.h"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/themes/lyra/Lyra3CoversTheme.h` around lines 1 - 5, The file
Lyra3CoversTheme.h contains extra leading blank lines before the `#pragma` once
directive; open the header (Lyra3CoversTheme.h) and remove any blank lines above
the `#pragma` once so the file starts immediately with `#pragma` once, keeping the
existing `#include` "components/themes/lyra/LyraTheme.h" unchanged.
src/activities/home/MyLibraryActivity.cpp (1)

191-197: Consider passing filename by const reference to avoid unnecessary copy.

The function takes std::string by value, creating a copy on each call. Since the string is only read, passing by const std::string& would be more efficient, especially when called repeatedly in list rendering.

♻️ Proposed fix
-std::string getFileName(std::string filename) {
+std::string getFileName(const std::string& filename) {
   if (filename.back() == '/') {
     return filename.substr(0, filename.length() - 1);
   }
   const auto pos = filename.rfind('.');
   return filename.substr(0, pos);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/activities/home/MyLibraryActivity.cpp` around lines 191 - 197, The
getFileName function currently takes filename by value, causing an unnecessary
copy on each call; change its signature to accept const std::string& filename
and update any internal uses accordingly (e.g., in getFileName and any callers
if needed) so the function reads from filename without copying; keep the same
logic for trimming a trailing '/' and finding the last '.' (ensure you still
handle std::string::npos correctly).
src/activities/settings/KOReaderAuthActivity.cpp (1)

104-109: Consider using metrics for vertical spacing instead of magic number.

The hardcoded + 10 spacing between lines could use metrics.verticalSpacing or a fraction thereof for consistency with the theme system used elsewhere in the file.

♻️ Proposed fix
   } else if (state == SUCCESS) {
     renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_SUCCESS), true, EpdFontFamily::BOLD);
-    renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, tr(STR_SYNC_READY));
+    renderer.drawCenteredText(UI_10_FONT_ID, top + height + metrics.verticalSpacing, tr(STR_SYNC_READY));
   } else if (state == FAILED) {
     renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_FAILED), true, EpdFontFamily::BOLD);
-    renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, errorMessage.c_str());
+    renderer.drawCenteredText(UI_10_FONT_ID, top + height + metrics.verticalSpacing, errorMessage.c_str());
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/activities/settings/KOReaderAuthActivity.cpp` around lines 104 - 109, The
code uses a magic "+ 10" vertical offset when calling renderer.drawCenteredText
for the second line in both SUCCESS and FAILED branches; replace that hardcoded
10 with the theme metric (e.g., metrics.verticalSpacing or the appropriate Theme
metrics variable used elsewhere in this file) so both calls use top + height +
metrics.verticalSpacing (or a fraction like metrics.verticalSpacing / 2 if
desired) to keep spacing consistent; update both renderer.drawCenteredText calls
that currently use "+ 10" to reference the metric instead.
src/components/themes/lyra/Lyra3CoversTheme.cpp (1)

17-21: File-scope mutable coverWidth is shadowed and unused.

The file-scope coverWidth variable at line 20 is shadowed by the local variable coverWidth declared at line 54. This makes the file-scope variable unused in this implementation while potentially causing confusion.

♻️ Proposed fix - remove unused file-scope variable
 namespace {
 constexpr int hPaddingInSelection = 8;
 constexpr int cornerRadius = 6;
-int coverWidth = 0;
 }  // namespace
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/themes/lyra/Lyra3CoversTheme.cpp` around lines 17 - 21, The
file-scope mutable variable coverWidth inside the anonymous namespace is
shadowed by a local coverWidth (declared near line 54) and is unused—remove the
file-scope coverWidth declaration to avoid confusion; keep the anonymous
namespace with the constexprs (hPaddingInSelection, cornerRadius) intact and
ensure all references use the local coverWidth in the function where it is
declared.
src/activities/util/KeyboardEntryActivity.cpp (1)

319-320: Consider using clearer symbols for vertical navigation hints.

The side button hints use ">" and "<" which typically indicate horizontal direction. For up/down navigation, consider using "^" / "v" or arrow unicode characters if supported by the font, to avoid confusion.

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

In `@src/activities/util/KeyboardEntryActivity.cpp` around lines 319 - 320,
Replace the horizontal indicators passed to GUI.drawSideButtonHints(renderer,
">", "<") with vertical navigation symbols to avoid confusion; update the call
to use "^" and "v" (or "↑" and "↓" if your font supports Unicode) so the side
button hints clearly indicate Up/Down navigation.
src/activities/network/CrossPointWebServerActivity.cpp (1)

366-386: Consider making drawQRCode static or moving to anonymous namespace.

The drawQRCode function is defined at file scope without static linkage. If another translation unit defines a function with the same name, this could cause ODR violations or linking errors.

🛠️ Proposed fix
-void drawQRCode(const GfxRenderer& renderer, const int x, const int y, const std::string& data) {
+namespace {
+void drawQRCode(const GfxRenderer& renderer, const int x, const int y, const std::string& data) {
   // Implementation...
 }
+}  // namespace
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/activities/network/CrossPointWebServerActivity.cpp` around lines 366 -
386, drawQRCode currently has external linkage at file scope which can cause
ODR/linkage conflicts; change its linkage to internal by either marking the
function static (add the static keyword to drawQRCode) or move its definition
into an anonymous namespace at top of file so it becomes translation-unit-local;
update any callers in this file to continue using drawQRCode and ensure symbols
like QRCode, qrcode_getBufferSize, qrcode_initText, and qrcode_getModule remain
visible in that scope.
src/components/themes/BaseTheme.cpp (1)

716-719: drawTextField ignores textWidth parameter for bracket positioning.

The textWidth parameter is accepted but the brackets are drawn at fixed positions (rect.x + 10 and rect.x + rect.width - 15) rather than being positioned relative to the text width. This differs from the LyraTheme implementation which uses textWidth to position a centered underline.

🤔 Consider aligning with LyraTheme approach

If the intent is to frame the text, consider using textWidth to position the brackets around the actual text content, or document the different behavior between themes.

🤖 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 716 - 719,
BaseTheme::drawTextField currently ignores the textWidth parameter and uses
fixed positions for brackets; change it to compute the text's horizontal origin
from textWidth (e.g., center the text inside rect or use the same centering
logic as LyraTheme) and place the left bracket relative to that text origin and
the right bracket at textOrigin + textWidth plus a small gap; update
renderer.drawText calls (the ones drawing "[" and "]") to use these computed
positions so brackets frame the actual text width instead of fixed offsets.
src/components/themes/BaseTheme.h (1)

64-64: Consider using enum class for type safety.

The UIIcon enum uses a plain enum which exposes its enumerators to the enclosing scope, potentially causing naming conflicts. Using enum class would provide stronger type safety and scoping.

♻️ Suggested change
-enum UIIcon { Folder, Text, Image, Book, File, Recent, Settings, Transfer, Library, Wifi, Hotspot };
+enum class UIIcon { Folder, Text, Image, Book, File, Recent, Settings, Transfer, Library, Wifi, Hotspot };

This would require updating call sites to use UIIcon::Folder etc., but provides better namespace hygiene and prevents implicit conversions to int.

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

In `@src/components/themes/BaseTheme.h` at line 64, Change the plain enum UIIcon
to a scoped enum (enum class UIIcon) to prevent its enumerators from leaking
into the enclosing scope; update all call sites that currently use raw
enumerators (e.g., Folder, Text, Image, etc.) to qualify them as UIIcon::Folder,
UIIcon::Text, UIIcon::Image, and adjust any implicit-int usage or comparisons
(casts to/from int) to use explicit casts or overloads so code using UIIcon (in
BaseTheme.h and its consumers) compiles cleanly.
src/components/themes/lyra/LyraTheme.cpp (1)

42-42: Mutable global state in anonymous namespace is problematic.

coverWidth is a mutable variable in the anonymous namespace, which means it persists across calls and could cause issues if multiple theme instances exist or if the cover dimensions change. This should be either:

  1. A member variable of LyraTheme, or
  2. Computed/passed each time it's needed
♻️ Suggested approach

Consider making coverWidth a mutable member of LyraTheme or computing it locally within drawRecentBookCover. The current approach caches the width globally after the first bitmap load, which could lead to stale values if covers of different sizes are loaded.

 // In anonymous namespace, remove:
-int coverWidth = 0;

 // In LyraTheme class (header), add:
+mutable int coverWidth_ = 0;

 // In drawRecentBookCover, update references:
-  if (coverWidth == 0) {
-    coverWidth = LyraMetrics::values.homeCoverHeight * 0.6;
+  if (coverWidth_ == 0) {
+    coverWidth_ = LyraMetrics::values.homeCoverHeight * 0.6;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/themes/lyra/LyraTheme.cpp` at line 42, The global mutable
coverWidth in the anonymous namespace should be removed and its value maintained
per-theme or computed on demand: make coverWidth a member variable of the
LyraTheme class (e.g., add an int coverWidth_ field) or compute the width inside
drawRecentBookCover each time by querying the bitmap (replace references to
coverWidth with either this->coverWidth_ or a local variable computed from the
loaded Bitmap), and update any initialization logic that currently caches the
width globally (e.g., where coverWidth is set after bitmap load) to set the new
member or local variable instead; update all usages in
LyraTheme::drawRecentBookCover and related LyraTheme methods to reference the
new member/local name instead of the anonymous-namespace coverWidth.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7cf2dc2 and edb541f.

📒 Files selected for processing (59)
  • lib/GfxRenderer/GfxRenderer.cpp
  • lib/I18n/I18nKeys.h
  • lib/I18n/translations/czech.yaml
  • lib/I18n/translations/english.yaml
  • lib/I18n/translations/french.yaml
  • lib/I18n/translations/german.yaml
  • lib/I18n/translations/portuguese.yaml
  • lib/I18n/translations/russian.yaml
  • lib/I18n/translations/spanish.yaml
  • lib/I18n/translations/swedish.yaml
  • lib/hal/HalDisplay.cpp
  • lib/hal/HalDisplay.h
  • open-x4-sdk
  • scripts/convert_icon.py
  • src/CrossPointSettings.h
  • src/SettingsList.h
  • src/activities/home/HomeActivity.cpp
  • src/activities/home/MyLibraryActivity.cpp
  • src/activities/home/RecentBooksActivity.cpp
  • src/activities/network/CalibreConnectActivity.cpp
  • src/activities/network/CrossPointWebServerActivity.cpp
  • src/activities/network/NetworkModeSelectionActivity.cpp
  • src/activities/network/WifiSelectionActivity.cpp
  • src/activities/network/WifiSelectionActivity.h
  • src/activities/settings/ButtonRemapActivity.cpp
  • src/activities/settings/CalibreSettingsActivity.cpp
  • src/activities/settings/CalibreSettingsActivity.h
  • src/activities/settings/ClearCacheActivity.cpp
  • src/activities/settings/KOReaderAuthActivity.cpp
  • src/activities/settings/KOReaderSettingsActivity.cpp
  • src/activities/settings/KOReaderSettingsActivity.h
  • src/activities/settings/OtaUpdateActivity.cpp
  • src/activities/settings/SettingsActivity.cpp
  • src/activities/util/KeyboardEntryActivity.cpp
  • src/activities/util/KeyboardEntryActivity.h
  • src/components/UITheme.cpp
  • src/components/UITheme.h
  • src/components/icons/book.h
  • src/components/icons/book24.h
  • src/components/icons/cog.h
  • src/components/icons/cover.h
  • src/components/icons/file24.h
  • src/components/icons/folder.h
  • src/components/icons/folder24.h
  • src/components/icons/hotspot.h
  • src/components/icons/image24.h
  • src/components/icons/library.h
  • src/components/icons/recent.h
  • src/components/icons/settings.h
  • src/components/icons/settings2.h
  • src/components/icons/text24.h
  • src/components/icons/transfer.h
  • src/components/icons/wifi.h
  • src/components/themes/BaseTheme.cpp
  • src/components/themes/BaseTheme.h
  • src/components/themes/lyra/Lyra3CoversTheme.cpp
  • src/components/themes/lyra/Lyra3CoversTheme.h
  • src/components/themes/lyra/LyraTheme.cpp
  • src/components/themes/lyra/LyraTheme.h
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/components/icons/book.h
  • src/components/UITheme.cpp
  • src/components/icons/folder.h
  • src/components/icons/text24.h
  • src/SettingsList.h
  • src/components/icons/recent.h
  • open-x4-sdk
  • src/components/icons/cog.h
  • src/components/icons/image24.h
  • src/activities/home/RecentBooksActivity.cpp
  • src/components/icons/book24.h
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-02-16T22:25:35.674Z
Learnt from: whyte-j
Repo: crosspoint-reader/crosspoint-reader PR: 733
File: lib/I18n/I18nKeys.h:271-272
Timestamp: 2026-02-16T22:25:35.674Z
Learning: In the crosspoint-reader i18n system (lib/I18n/), missing translation keys in non-English YAML files are automatically filled with English strings at build time by the gen_i18n.py script. All generated string arrays (STRINGS_EN, STRINGS_ES, etc.) have identical lengths, so runtime array indexing is safe without per-string fallback logic. The system prints "INFO: '{key}' missing in {lang_code}, using English fallback" during code generation when this occurs.

Applied to files:

  • lib/I18n/translations/spanish.yaml
  • lib/I18n/translations/english.yaml
📚 Learning: 2026-02-12T06:57:35.955Z
Learnt from: CaptainFrito
Repo: crosspoint-reader/crosspoint-reader PR: 725
File: src/activities/network/CalibreConnectActivity.cpp:218-264
Timestamp: 2026-02-12T06:57:35.955Z
Learning: In src/activities/network/CalibreConnectActivity.cpp, button hints (« Exit) are intentionally omitted from the SERVER_STARTING and ERROR states—only the SERVER_RUNNING state displays navigation hints. This is a deliberate design decision by the author.

Applied to files:

  • src/activities/settings/ButtonRemapActivity.cpp
  • src/activities/network/NetworkModeSelectionActivity.cpp
  • src/activities/network/CalibreConnectActivity.cpp
  • src/activities/util/KeyboardEntryActivity.cpp
  • src/activities/network/CrossPointWebServerActivity.cpp
  • src/activities/settings/KOReaderAuthActivity.cpp
  • src/activities/settings/CalibreSettingsActivity.cpp
  • src/activities/settings/OtaUpdateActivity.cpp
📚 Learning: 2026-02-16T20:43:19.339Z
Learnt from: Levrk
Repo: crosspoint-reader/crosspoint-reader PR: 909
File: src/activities/reader/EpubReaderActivity.cpp:461-461
Timestamp: 2026-02-16T20:43:19.339Z
Learning: In src/activities/reader/EpubReaderActivity.cpp, when using ConfirmationActivity with enterNewActivity(), it's not necessary to call exitActivity() beforehand. The ConfirmationActivity operates as a modal dialog and cleanup is handled via the pendingSubactivityExit flag in the callback lambda. This differs from other activities like EpubReaderChapterSelectionActivity or KOReaderSyncActivity which do require exitActivity() before enterNewActivity().

Applied to files:

  • src/activities/settings/ClearCacheActivity.cpp
  • src/activities/network/CalibreConnectActivity.cpp
  • src/activities/util/KeyboardEntryActivity.h
  • src/activities/settings/KOReaderAuthActivity.cpp
  • src/activities/settings/CalibreSettingsActivity.cpp
  • src/activities/settings/KOReaderSettingsActivity.cpp
🧬 Code graph analysis (17)
src/activities/settings/ButtonRemapActivity.cpp (3)
src/CrossPointSettings.h (1)
  • getInstance (181-181)
lib/I18n/I18n.cpp (2)
  • getInstance (15-18)
  • getInstance (15-15)
src/main.cpp (1)
  • renderer (37-37)
src/activities/network/NetworkModeSelectionActivity.cpp (2)
src/components/UITheme.h (1)
  • getInstance (17-17)
lib/I18n/I18n.cpp (2)
  • getInstance (15-18)
  • getInstance (15-15)
src/components/themes/lyra/Lyra3CoversTheme.h (3)
src/components/themes/lyra/LyraTheme.h (1)
  • LyraTheme (39-68)
src/components/themes/BaseTheme.cpp (2)
  • drawRecentBookCover (341-634)
  • drawRecentBookCover (341-343)
src/components/themes/lyra/Lyra3CoversTheme.cpp (2)
  • drawRecentBookCover (23-110)
  • drawRecentBookCover (23-25)
lib/hal/HalDisplay.h (1)
lib/hal/HalDisplay.cpp (2)
  • drawImageTransparent (19-22)
  • drawImageTransparent (19-20)
src/activities/util/KeyboardEntryActivity.cpp (2)
src/components/UITheme.h (1)
  • getInstance (17-17)
lib/I18n/I18n.cpp (2)
  • getInstance (15-18)
  • getInstance (15-15)
src/activities/settings/KOReaderAuthActivity.cpp (1)
src/components/UITheme.h (1)
  • getInstance (17-17)
src/components/UITheme.h (1)
src/components/UITheme.cpp (2)
  • getFileIcon (77-92)
  • getFileIcon (77-77)
src/components/themes/lyra/Lyra3CoversTheme.cpp (4)
src/components/themes/BaseTheme.cpp (3)
  • drawRecentBookCover (341-634)
  • drawRecentBookCover (341-343)
  • bitmap (370-370)
src/components/themes/lyra/LyraTheme.cpp (5)
  • drawRecentBookCover (411-493)
  • drawRecentBookCover (411-413)
  • bitmap (439-439)
  • drawEmptyRecents (495-501)
  • drawEmptyRecents (495-495)
src/activities/home/HomeActivity.cpp (2)
  • storeCoverBuffer (134-151)
  • storeCoverBuffer (134-134)
src/components/UITheme.cpp (2)
  • getCoverThumbPath (69-75)
  • getCoverThumbPath (69-69)
src/activities/settings/CalibreSettingsActivity.cpp (3)
src/components/UITheme.h (1)
  • getInstance (17-17)
lib/I18n/I18n.cpp (2)
  • getInstance (15-18)
  • getInstance (15-15)
src/SettingsList.h (3)
  • string (88-91)
  • string (95-98)
  • string (102-105)
src/components/themes/lyra/LyraTheme.h (1)
src/components/themes/lyra/LyraTheme.cpp (10)
  • drawHeader (165-194)
  • drawHeader (165-165)
  • drawList (247-338)
  • drawList (247-251)
  • drawEmptyRecents (495-501)
  • drawEmptyRecents (495-495)
  • drawTextField (556-560)
  • drawTextField (556-556)
  • drawKeyboardKey (562-572)
  • drawKeyboardKey (562-563)
src/activities/settings/KOReaderSettingsActivity.cpp (4)
src/components/UITheme.h (1)
  • getInstance (17-17)
lib/I18n/I18n.cpp (2)
  • getInstance (15-18)
  • getInstance (15-15)
lib/KOReaderSync/KOReaderCredentialStore.h (1)
  • getInstance (36-36)
src/SettingsList.h (3)
  • string (88-91)
  • string (95-98)
  • string (102-105)
src/activities/network/WifiSelectionActivity.cpp (1)
src/components/UITheme.h (1)
  • getInstance (17-17)
src/activities/settings/OtaUpdateActivity.cpp (1)
src/components/UITheme.h (1)
  • getInstance (17-17)
src/components/themes/lyra/LyraTheme.cpp (3)
src/components/themes/BaseTheme.cpp (2)
  • drawButtonMenu (636-663)
  • drawButtonMenu (636-638)
src/components/UITheme.cpp (2)
  • getCoverThumbPath (69-75)
  • getCoverThumbPath (69-69)
src/activities/home/HomeActivity.cpp (2)
  • storeCoverBuffer (134-151)
  • storeCoverBuffer (134-134)
src/activities/home/MyLibraryActivity.cpp (1)
src/components/UITheme.cpp (2)
  • getFileIcon (77-92)
  • getFileIcon (77-77)
src/components/themes/BaseTheme.cpp (1)
src/components/themes/lyra/LyraTheme.cpp (8)
  • drawHeader (165-194)
  • drawHeader (165-165)
  • drawSubHeader (196-213)
  • drawSubHeader (196-196)
  • drawTextField (556-560)
  • drawTextField (556-556)
  • drawKeyboardKey (562-572)
  • drawKeyboardKey (562-563)
src/components/themes/BaseTheme.h (1)
src/components/themes/BaseTheme.cpp (22)
  • drawHeader (256-286)
  • drawHeader (256-256)
  • drawSubHeader (288-307)
  • drawSubHeader (288-288)
  • drawTabBar (309-337)
  • drawTabBar (309-310)
  • drawRecentBookCover (341-634)
  • drawRecentBookCover (341-343)
  • drawButtonMenu (636-663)
  • drawButtonMenu (636-638)
  • drawPopup (665-682)
  • drawPopup (665-665)
  • fillPopupProgress (684-695)
  • fillPopupProgress (684-684)
  • drawReadingProgressBar (697-707)
  • drawReadingProgressBar (697-697)
  • drawHelpText (709-714)
  • drawHelpText (709-709)
  • drawTextField (716-719)
  • drawTextField (716-716)
  • drawKeyboardKey (721-730)
  • drawKeyboardKey (721-722)
🪛 Ruff (0.15.0)
scripts/convert_icon.py

[error] 50-50: f-string without any placeholders

Remove extraneous f prefix

(F541)

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/convert_icon.py`:
- Line 50: The string assignment to variable c uses an unnecessary f-string
prefix despite containing no placeholders; remove the leading 'f' from the
literal assigned to c (the assignment "c = f'#pragma once\n#include
<cstdint>\n\n'") so it becomes a plain string literal, ensuring code uses
regular string formatting where needed and clears the static analysis warning.

In `@src/activities/network/CalibreConnectActivity.cpp`:
- Around line 195-198: The instruction block uses SMALL_FONT_ID for drawText but
uses the wrong line height variable (currently using height derived from
UI_10_FONT_ID); update the vertical spacing for the four renderer.drawText calls
so they use renderer.getLineHeight(SMALL_FONT_ID) instead of the existing height
value to compute y offsets (i.e., replace uses of height in that instruction
block with renderer.getLineHeight(SMALL_FONT_ID)) so spacing matches the
SMALL_FONT_ID font metrics.

In `@src/activities/network/CrossPointWebServerActivity.cpp`:
- Around line 388-396: The header/subheader are being drawn twice: once in
render() and again inside renderServerRunning(); remove the duplicated
GUI.drawHeader(...) and GUI.drawSubHeader(...) calls from renderServerRunning()
so that only render() performs header/subheader rendering (keep
renderServerRunning() responsible for the server-specific body UI like
connectedSSID usage and body content); update renderServerRunning() to rely on
render() for headers and only draw its unique elements, leaving symbols
renderServerRunning(), render(), GUI.drawHeader, GUI.drawSubHeader, isApMode,
and connectedSSID as references to locate the code to change.

In `@src/activities/network/WifiSelectionActivity.h`:
- Line 48: selectedNetworkIndex is size_t but selectNetwork currently takes an
int; change selectNetwork(int index) to selectNetwork(size_t index) to align
types and improve safety, then update all callers: where ButtonNavigator returns
int (e.g., usages that pass its return directly), perform an explicit negative
check before converting or cast the int to size_t with a clear guard, and remove
any casts that convert size_t to int when calling selectNetwork; reference the
symbols selectedNetworkIndex and selectNetwork to locate the changes and ensure
all prototypes/signatures and declarations are updated consistently (header and
implementation).

---

Outside diff comments:
In `@src/activities/settings/OtaUpdateActivity.cpp`:
- Around line 99-105: The division by updater.getTotalSize() in
OtaUpdateActivity.cpp can divide by zero; modify the updater progress
calculation (where updaterProgress, updater.getProcessedSize(),
updater.getTotalSize(), and lastUpdaterPercentage are used) to first check that
updater.getTotalSize() > 0 (or >= 1) and only compute the ratio when positive;
if total is zero or negative, set updaterProgress to 0 (or an appropriate
sentinel) and skip the percentage-based early return/update logic so subsequent
casts to int and comparisons cannot use NaN/inf.

---

Duplicate comments:
In `@scripts/convert_icon.py`:
- Around line 15-27: The SVG branch in load_image currently opens the PNG bytes
but doesn't flatten alpha onto a white background like the raster branch; update
the SVG path (after calling svg_to_png_bytes and Image.open) to mirror the
raster handling: convert to 'RGBA', resize to (width, height) with Image.LANCZOS
if needed, create a new white RGBA background, paste the image onto it using the
image alpha channel as mask, and set img to that flattened background so
downstream grayscale/thresholding won't produce dark transparent pixels.
- Around line 61-66: Update the usage string to reference convert_icon.py and
sanitize the C identifier derived from output_name before creating array_name:
replace/normalize invalid characters (e.g., convert hyphens and other
non-alphanumeric chars to underscores), ensure it doesn't start with a digit
(prefix with an underscore or a letter if it does), and then append or format
the suffix (use a safe capitalization like title-casing alphanumeric segments)
to produce array_name (currently created from output_name.capitalize() =>
array_name). Implement this normalization where input_path, output_name, width,
height are unpacked so array_name is always a valid C identifier.

In `@src/activities/settings/OtaUpdateActivity.cpp`:
- Around line 135-138: The NO_UPDATE and FAILED branches render centered text
but omit the Back button hint displayed in other states; update the branches in
OtaUpdateActivity.cpp (the else if (state == NO_UPDATE) and else if (state ==
FAILED) blocks that call renderer.drawCenteredText) to also render the Back hint
using the same renderer button-hint call used elsewhere in this file (so the UI
matches the loop() Back handling), keeping the hint text/style consistent with
the other state branches.

In `@src/components/themes/BaseTheme.cpp`:
- Line 91: The log call uses wrong format specifiers for size_t: update the
LOG_DBG invocation that logs current, total, percent (the call where
LOG_DBG("UI", "Drawing progress bar: current=%u, total=%u, percent=%d", current,
total, percent)) to use safe specifiers for size_t (e.g., "%zu" for current and
total) or explicitly cast current and total to a fixed-width integer type (e.g.,
(unsigned long) or (uint32_t)) and adjust the format string accordingly so the
types and specifiers match.
- Line 22: The hard-coded constexpr subtitleY = 738 in BaseTheme.cpp causes
misplacement when header height or screen size changes; replace this fixed
constant with a computed value at layout time (not a compile-time constexpr)
that derives subtitleY from the actual header height or screen dimensions (e.g.,
subtitleY = headerBottom + desiredOffset or subtitleY = screenHeight *
relativeFactor), using the existing header height accessor or screen size API
your codebase provides; update the code that references subtitleY to use the
computed variable (instead of the constexpr) so subtitle positioning adapts to
dynamic header/screen changes.

In `@src/components/themes/lyra/Lyra3CoversTheme.cpp`:
- Around line 70-78: The empty-cover rendering block uses
LyraMetrics::values.homeCoverHeight instead of the theme-specific
Lyra3CoversMetrics::values.homeCoverHeight; update all occurrences in the
conditional that draws the empty cover (the calls to renderer.drawRect,
renderer.fillRect, and any position math that references
LyraMetrics::values.homeCoverHeight) to use
Lyra3CoversMetrics::values.homeCoverHeight so the Lyra3CoversTheme uses its own
metrics for height and vertical calculations.

---

Nitpick comments:
In `@lib/GfxRenderer/GfxRenderer.cpp`:
- Around line 323-325: The current comment misstates the condition used to
compute roundedSides: the code sets roundedSides based on (!roundTopLeft ||
!roundTopRight || !roundBottomLeft || !roundBottomRight) which triggers when any
corner is not rounded, so update the comment to clearly describe this heuristic
and its limitations (it assumes either all corners or effectively one side is
rounded and may produce odd results for diagonal or three-corner cases) and
either (A) document that diagonal/three-corner cases are unsupported, or (B)
replace the heuristic by computing an explicit roundedCount (e.g., count true
corner flags) and use that to derive roundedSides/maxRadius to handle mixed
cases; reference the roundedSides, cornerRadius and maxRadius symbols when
making the change.

In `@src/activities/home/MyLibraryActivity.cpp`:
- Around line 191-197: The getFileName function currently takes filename by
value, causing an unnecessary copy on each call; change its signature to accept
const std::string& filename and update any internal uses accordingly (e.g., in
getFileName and any callers if needed) so the function reads from filename
without copying; keep the same logic for trimming a trailing '/' and finding the
last '.' (ensure you still handle std::string::npos correctly).

In `@src/activities/network/CrossPointWebServerActivity.cpp`:
- Around line 366-386: drawQRCode currently has external linkage at file scope
which can cause ODR/linkage conflicts; change its linkage to internal by either
marking the function static (add the static keyword to drawQRCode) or move its
definition into an anonymous namespace at top of file so it becomes
translation-unit-local; update any callers in this file to continue using
drawQRCode and ensure symbols like QRCode, qrcode_getBufferSize,
qrcode_initText, and qrcode_getModule remain visible in that scope.

In `@src/activities/settings/ButtonRemapActivity.cpp`:
- Around line 136-141: The help text Y offsets are hard-coded with “4 *
metrics.listRowHeight” and “5 * metrics.verticalSpacing” which assumes 4 list
items; update the calculation to derive the vertical position from the actual
list bounds or the canonical role count instead of magic numbers: use the list's
computed bounds (e.g. the variable that holds the list Rect or call that
computes its height) or replace the literal multipliers with kRoleCount *
metrics.listRowHeight and kRoleCount * metrics.verticalSpacing so
GUI.drawHelpText calls compute topOffset + listBounds.height (or topOffset +
kRoleCount * metrics.listRowHeight) + additional spacing for the two help lines,
referencing GUI.drawHelpText, metrics.listRowHeight, metrics.verticalSpacing,
and kRoleCount (or the list bounds variable) to locate and update the code.

In `@src/activities/settings/KOReaderAuthActivity.cpp`:
- Around line 104-109: The code uses a magic "+ 10" vertical offset when calling
renderer.drawCenteredText for the second line in both SUCCESS and FAILED
branches; replace that hardcoded 10 with the theme metric (e.g.,
metrics.verticalSpacing or the appropriate Theme metrics variable used elsewhere
in this file) so both calls use top + height + metrics.verticalSpacing (or a
fraction like metrics.verticalSpacing / 2 if desired) to keep spacing
consistent; update both renderer.drawCenteredText calls that currently use "+
10" to reference the metric instead.

In `@src/activities/settings/OtaUpdateActivity.cpp`:
- Around line 87-105: Compute updaterProgress and perform the 2% throttle check
before doing any heavy rendering (i.e., before calling renderer.clearScreen()
and GUI.drawHeader()). Specifically, call updater.getProcessedSize() and
updater.getTotalSize(), compute updaterProgress, compare the throttled
percentage against lastUpdaterPercentage, and return early if unchanged; only if
the check passes proceed to clear the screen, draw the header and continue
updating lastUpdaterPercentage. Update references to updaterProgress,
lastUpdaterPercentage, renderer.clearScreen(), GUI.drawHeader(),
updater.getProcessedSize(), and updater.getTotalSize() accordingly so no
unnecessary work is done when throttled.

In `@src/activities/util/KeyboardEntryActivity.cpp`:
- Around line 319-320: Replace the horizontal indicators passed to
GUI.drawSideButtonHints(renderer, ">", "<") with vertical navigation symbols to
avoid confusion; update the call to use "^" and "v" (or "↑" and "↓" if your font
supports Unicode) so the side button hints clearly indicate Up/Down navigation.

In `@src/components/themes/BaseTheme.cpp`:
- Around line 716-719: BaseTheme::drawTextField currently ignores the textWidth
parameter and uses fixed positions for brackets; change it to compute the text's
horizontal origin from textWidth (e.g., center the text inside rect or use the
same centering logic as LyraTheme) and place the left bracket relative to that
text origin and the right bracket at textOrigin + textWidth plus a small gap;
update renderer.drawText calls (the ones drawing "[" and "]") to use these
computed positions so brackets frame the actual text width instead of fixed
offsets.

In `@src/components/themes/BaseTheme.h`:
- Line 64: Change the plain enum UIIcon to a scoped enum (enum class UIIcon) to
prevent its enumerators from leaking into the enclosing scope; update all call
sites that currently use raw enumerators (e.g., Folder, Text, Image, etc.) to
qualify them as UIIcon::Folder, UIIcon::Text, UIIcon::Image, and adjust any
implicit-int usage or comparisons (casts to/from int) to use explicit casts or
overloads so code using UIIcon (in BaseTheme.h and its consumers) compiles
cleanly.

In `@src/components/themes/lyra/Lyra3CoversTheme.cpp`:
- Around line 17-21: The file-scope mutable variable coverWidth inside the
anonymous namespace is shadowed by a local coverWidth (declared near line 54)
and is unused—remove the file-scope coverWidth declaration to avoid confusion;
keep the anonymous namespace with the constexprs (hPaddingInSelection,
cornerRadius) intact and ensure all references use the local coverWidth in the
function where it is declared.

In `@src/components/themes/lyra/Lyra3CoversTheme.h`:
- Around line 1-5: The file Lyra3CoversTheme.h contains extra leading blank
lines before the `#pragma` once directive; open the header (Lyra3CoversTheme.h)
and remove any blank lines above the `#pragma` once so the file starts immediately
with `#pragma` once, keeping the existing `#include`
"components/themes/lyra/LyraTheme.h" unchanged.

In `@src/components/themes/lyra/LyraTheme.cpp`:
- Line 42: The global mutable coverWidth in the anonymous namespace should be
removed and its value maintained per-theme or computed on demand: make
coverWidth a member variable of the LyraTheme class (e.g., add an int
coverWidth_ field) or compute the width inside drawRecentBookCover each time by
querying the bitmap (replace references to coverWidth with either
this->coverWidth_ or a local variable computed from the loaded Bitmap), and
update any initialization logic that currently caches the width globally (e.g.,
where coverWidth is set after bitmap load) to set the new member or local
variable instead; update all usages in LyraTheme::drawRecentBookCover and
related LyraTheme methods to reference the new member/local name instead of the
anonymous-namespace coverWidth.

In `@src/components/UITheme.h`:
- Line 26: Change the getFileIcon API to accept the filename by const reference
to avoid copies: update the declaration static UIIcon getFileIcon(const
std::string& filename) in UITheme.h and update the corresponding definition in
UITheme.cpp to match (function name getFileIcon). Also update any call sites
that relied on the by-value signature (pass std::string or const std::string& as
before) to ensure they compile. Rebuild and run the file browser performance
regression test to confirm improvement.
- Around line 9-10: Remove the unused forward declaration of MappedInputManager
from UITheme.h; if the type is only referenced in UITheme.cpp, move the forward
declaration or the `#include` for MappedInputManager into UITheme.cpp (or include
the header where it is actually used) so UITheme.h no longer contains an unused
forward declaration.

byte |= (bit << (7 - b))
packed.append(byte)
# Format as C array
c = f'#pragma once\n#include <cstdint>\n\n'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove extraneous f prefix from string literal.

This string has no placeholders, so the f prefix is unnecessary and was flagged by static analysis.

🔧 Proposed fix
-    c = f'#pragma once\n#include <cstdint>\n\n'
+    c = '#pragma once\n#include <cstdint>\n\n'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
c = f'#pragma once\n#include <cstdint>\n\n'
c = '#pragma once\n#include <cstdint>\n\n'
🧰 Tools
🪛 Ruff (0.15.0)

[error] 50-50: f-string without any placeholders

Remove extraneous f prefix

(F541)

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

In `@scripts/convert_icon.py` at line 50, The string assignment to variable c uses
an unnecessary f-string prefix despite containing no placeholders; remove the
leading 'f' from the literal assigned to c (the assignment "c = f'#pragma
once\n#include <cstdint>\n\n'") so it becomes a plain string literal, ensuring
code uses regular string formatting where needed and clears the static analysis
warning.

Comment on lines +195 to +198
renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y, tr(STR_CALIBRE_INSTRUCTION_1));
renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height, tr(STR_CALIBRE_INSTRUCTION_2));
renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height * 2, tr(STR_CALIBRE_INSTRUCTION_3));
renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height * 3, tr(STR_CALIBRE_INSTRUCTION_4));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find font ID definitions
rg -n "SMALL_FONT_ID|UI_10_FONT_ID" --type-add 'header:*.h' --type-add 'cpp:*.cpp' | head -50

Repository: crosspoint-reader/crosspoint-reader

Length of output: 6816


🏁 Script executed:

# Check the specific file and lines around the height calculation
cat -n src/activities/network/CalibreConnectActivity.cpp | sed -n '170,200p'

Repository: crosspoint-reader/crosspoint-reader

Length of output: 2185


🏁 Script executed:

# Find the font family definitions in main.cpp
cat -n src/main.cpp | sed -n '270,285p'

Repository: crosspoint-reader/crosspoint-reader

Length of output: 821


🏁 Script executed:

# Search for font ID definitions
fd -e h . | xargs grep -l "SMALL_FONT_ID\|UI_10_FONT_ID" | head -5

Repository: crosspoint-reader/crosspoint-reader

Length of output: 92


🏁 Script executed:

# Check if there's a fonts definitions file
fd -e h . | xargs grep -l "ui10FontFamily\|smallFontFamily" 2>/dev/null

Repository: crosspoint-reader/crosspoint-reader

Length of output: 63


🏁 Script executed:

# Check fontIds.h for font ID definitions
cat src/fontIds.h

Repository: crosspoint-reader/crosspoint-reader

Length of output: 796


🏁 Script executed:

# Search for where ui10FontFamily and smallFontFamily are defined
rg -n "ui10FontFamily|smallFontFamily" src/main.cpp | head -20

Repository: crosspoint-reader/crosspoint-reader

Length of output: 309


🏁 Script executed:

# Look for font family definitions in main.cpp earlier lines
cat -n src/main.cpp | sed -n '1,100p' | grep -i "font"

Repository: crosspoint-reader/crosspoint-reader

Length of output: 4185


🏁 Script executed:

# Find smallFont and ui10RegularFont definitions
rg -n "EpdFont smallFont|EpdFont ui10RegularFont" src/main.cpp

Repository: crosspoint-reader/crosspoint-reader

Length of output: 171


🏁 Script executed:

# Look for these font definitions - they might be in builtinFonts headers
cat -n src/main.cpp | sed -n '100,130p'

Repository: crosspoint-reader/crosspoint-reader

Length of output: 1964


🏁 Script executed:

# Verify the actual line heights by checking if there are any comments or docs about font metrics
rg -n "getLineHeight|getTextHeight" src/activities/network/CalibreConnectActivity.cpp

Repository: crosspoint-reader/crosspoint-reader

Length of output: 216


Use the correct font's line height for instruction text spacing.

Lines 195-198 use SMALL_FONT_ID (Noto Sans 8pt) with vertical spacing calculated from UI_10_FONT_ID line height (Ubuntu 10pt). Since these are different fonts with different point sizes, they will have different line heights, resulting in incorrect spacing. Replace height with renderer.getLineHeight(SMALL_FONT_ID) for the instruction block spacing.

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

In `@src/activities/network/CalibreConnectActivity.cpp` around lines 195 - 198,
The instruction block uses SMALL_FONT_ID for drawText but uses the wrong line
height variable (currently using height derived from UI_10_FONT_ID); update the
vertical spacing for the four renderer.drawText calls so they use
renderer.getLineHeight(SMALL_FONT_ID) instead of the existing height value to
compute y offsets (i.e., replace uses of height in that instruction block with
renderer.getLineHeight(SMALL_FONT_ID)) so spacing matches the SMALL_FONT_ID font
metrics.

Comment on lines 388 to 396
void CrossPointWebServerActivity::renderServerRunning() const {
// Use consistent line spacing
constexpr int LINE_SPACING = 28; // Space between lines
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();

renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_FILE_TRANSFER), true, EpdFontFamily::BOLD);
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight},
isApMode ? tr(STR_HOTSPOT_MODE) : tr(STR_FILE_TRANSFER), nullptr);
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
connectedSSID.c_str());

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate header/subheader rendering detected.

The header and subheader are drawn twice:

  1. In render() at lines 350-355
  2. Again in renderServerRunning() at lines 392-395

This causes unnecessary rendering overhead and potential visual artifacts if the two calls produce slightly different results.

🐛 Proposed fix - remove duplicate from renderServerRunning
 void CrossPointWebServerActivity::renderServerRunning() const {
   auto metrics = UITheme::getInstance().getMetrics();
   const auto pageWidth = renderer.getScreenWidth();

-  GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight},
-                 isApMode ? tr(STR_HOTSPOT_MODE) : tr(STR_FILE_TRANSFER), nullptr);
-  GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
-                    connectedSSID.c_str());
-
   int startY = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 2;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/activities/network/CrossPointWebServerActivity.cpp` around lines 388 -
396, The header/subheader are being drawn twice: once in render() and again
inside renderServerRunning(); remove the duplicated GUI.drawHeader(...) and
GUI.drawSubHeader(...) calls from renderServerRunning() so that only render()
performs header/subheader rendering (keep renderServerRunning() responsible for
the server-specific body UI like connectedSSID usage and body content); update
renderServerRunning() to rely on render() for headers and only draw its unique
elements, leaving symbols renderServerRunning(), render(), GUI.drawHeader,
GUI.drawSubHeader, isApMode, and connectedSSID as references to locate the code
to change.


WifiSelectionState state = WifiSelectionState::SCANNING;
int selectedNetworkIndex = 0;
size_t selectedNetworkIndex = 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, locate the file and examine the relevant lines
find . -name "WifiSelectionActivity.h" -type f

Repository: crosspoint-reader/crosspoint-reader

Length of output: 127


🏁 Script executed:

# Check the file content around the mentioned lines
head -100 src/activities/network/WifiSelectionActivity.h | tail -60

Repository: crosspoint-reader/crosspoint-reader

Length of output: 2021


🏁 Script executed:

# Search for selectNetwork method declarations and call sites
rg -nP --type=cpp -B2 -A2 'selectNetwork\s*\('

Repository: crosspoint-reader/crosspoint-reader

Length of output: 1342


🏁 Script executed:

# Search for selectedNetworkIndex usage
rg -nP --type=cpp -B2 -A2 'selectedNetworkIndex'

Repository: crosspoint-reader/crosspoint-reader

Length of output: 4456


🏁 Script executed:

# Find ButtonNavigator implementation
find . -name "*ButtonNavigator*" -type f

Repository: crosspoint-reader/crosspoint-reader

Length of output: 138


🏁 Script executed:

# Search for nextIndex and previousIndex implementations
rg -nP --type=cpp -B2 -A5 'nextIndex|previousIndex' --max-count=10

Repository: crosspoint-reader/crosspoint-reader

Length of output: 17931


Align parameter type with size_t index variable.

selectedNetworkIndex uses size_t, but selectNetwork(int) accepts a signed int parameter. While bounds checking in selectNetwork() prevents invalid access, the implicit conversions at lines 439 and 444 (ButtonNavigator returns int) and line 413 (passing size_t to int) introduce unnecessary type mismatches. Aligning the parameter to size_t improves type safety and clarity.

🔧 Suggested header alignment
-  void selectNetwork(int index);
+  void selectNetwork(size_t index);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/activities/network/WifiSelectionActivity.h` at line 48,
selectedNetworkIndex is size_t but selectNetwork currently takes an int; change
selectNetwork(int index) to selectNetwork(size_t index) to align types and
improve safety, then update all callers: where ButtonNavigator returns int
(e.g., usages that pass its return directly), perform an explicit negative check
before converting or cast the int to size_t with a clear guard, and remove any
casts that convert size_t to int when calling selectNetwork; reference the
symbols selectedNetworkIndex and selectNetwork to locate the changes and ensure
all prototypes/signatures and declarations are updated consistently (header and
implementation).

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 (4)
src/components/themes/lyra/LyraTheme.cpp (2)

451-459: Consider extracting magic numbers for icon positioning.

The icon offset 24 appears both here and in Lyra3CoversTheme.cpp (line 75). Consider defining a constant like constexpr int coverIconOffset = 24; for consistency and maintainability.

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

In `@src/components/themes/lyra/LyraTheme.cpp` around lines 451 - 459, The
drawIcon call in the hasCover false branch uses the magic number 24 for X/Y
offsets (renderer.drawIcon(CoverIcon, ... + 24, ... + 24, 32, 32)); introduce a
named constant (e.g., constexpr int coverIconOffset = 24) in the Lyra theme
source or header shared with Lyra3CoversTheme, replace the hardcoded 24s with
coverIconOffset in LyraTheme.cpp (and update Lyra3CoversTheme.cpp line 75 to use
the same constant) so positioning is consistent and maintainable while leaving
the existing renderer.drawIcon and CoverIcon usage unchanged.

39-42: Remove unused mainMenuColumns constant or add a clarifying comment.

The constant mainMenuColumns = 2 at line 41 is never referenced anywhere in the codebase. Either remove it or add a comment explaining its intended purpose if it's planned for future use.

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

In `@src/components/themes/lyra/LyraTheme.cpp` around lines 39 - 42, The constant
mainMenuColumns in LyraTheme.cpp is unused; either remove the declaration
constexpr int mainMenuColumns = 2 from LyraTheme.cpp, or if it’s intended for
future layout logic, keep it but add a one-line comment above the declaration
explaining its intended purpose (e.g., "number of columns for main menu layout -
reserved for future use") so its presence is intentional and clear; update any
relevant unit tests or style checks if they expect no unused symbols.
src/components/themes/BaseTheme.h (1)

64-65: Consider using enum class for stronger type safety.

The plain enum UIIcon places its enumerators (e.g., File, Text, Image) in the enclosing namespace. These generic names risk collisions with other identifiers. Using enum class UIIcon would scope the values and require explicit qualification (e.g., UIIcon::File).

♻️ Proposed refactor
-enum UIIcon { Folder, Text, Image, Book, File, Recent, Settings, Transfer, Library, Wifi, Hotspot };
+enum class UIIcon { Folder, Text, Image, Book, File, Recent, Settings, Transfer, Library, Wifi, Hotspot };

Note: This would require updating all usages to use the UIIcon:: prefix (which already appears to be the convention in the codebase based on the LyraTheme.cpp usage).

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

In `@src/components/themes/BaseTheme.h` around lines 64 - 65, Change the plain
enum UIIcon to a scoped enum (enum class UIIcon) to prevent polluting the
enclosing namespace; update all call sites to use the scoped qualifiers (e.g.,
UIIcon::File, UIIcon::Text, UIIcon::Image, etc.), including usage in
LyraTheme.cpp and any switch/statements or variable declarations that currently
reference unqualified enumerators, and ensure any implicit conversions to/from
integers are made explicit where needed.
src/components/UITheme.h (1)

25-25: Use const reference to avoid unnecessary copies in hot path.

The signature takes std::string by value, which creates a copy on every call. Since this function is called in the rendering loop (both RecentBooksActivity::render() and MyLibraryActivity::render()), it's a hot path. The implementation only reads the filename (checking extensions and directory status), never modifies it, making const std::string& the appropriate choice.

Update both the declaration and definition:

Required changes
-  static UIIcon getFileIcon(std::string filename);
+  static UIIcon getFileIcon(const std::string& filename);
-UIIcon UITheme::getFileIcon(std::string filename) {
+UIIcon UITheme::getFileIcon(const std::string& filename) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/UITheme.h` at line 25, Change the hot-path function UIIcon
getFileIcon(std::string filename) to accept a const reference: UIIcon
getFileIcon(const std::string& filename); update the corresponding definition
implementation (the function with the same name) to match the new signature and
ensure all callers (e.g., RecentBooksActivity::render and
MyLibraryActivity::render) continue to compile; this avoids unnecessary copies
on each render while preserving const-correctness since the function only reads
filename.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between edb541f and 7ebfea9.

📒 Files selected for processing (9)
  • lib/GfxRenderer/GfxRenderer.cpp
  • src/activities/home/HomeActivity.cpp
  • src/activities/home/MyLibraryActivity.cpp
  • src/components/UITheme.h
  • src/components/themes/BaseTheme.cpp
  • src/components/themes/BaseTheme.h
  • src/components/themes/lyra/Lyra3CoversTheme.cpp
  • src/components/themes/lyra/LyraTheme.cpp
  • src/components/themes/lyra/LyraTheme.h
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/GfxRenderer/GfxRenderer.cpp
  • src/activities/home/MyLibraryActivity.cpp
  • src/components/themes/lyra/LyraTheme.h
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/UITheme.h (1)
src/components/UITheme.cpp (2)
  • getFileIcon (77-92)
  • getFileIcon (77-77)
⏰ 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 comments (9)
src/activities/home/HomeActivity.cpp (2)

232-237: LGTM!

The menuIcons vector is correctly aligned with menuItems, and the OPDS Library icon is inserted at the same index (2) as the menu label, maintaining proper correspondence between items and icons.


240-247: LGTM!

The lambdas correctly capture local vectors by reference for use within the synchronous drawButtonMenu call. The icons lambda follows the same pattern as the existing label-mapper lambda.

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

68-76: LGTM! Empty cover placeholder rendering with icon.

The empty cover rendering correctly uses Lyra3CoversMetrics::values.homeCoverHeight consistently, and the CoverIcon provides visual feedback when no cover is available. The past review concern about inconsistent metrics namespace has been addressed.

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

113-118: LGTM! Updated method signatures for icon support.

The drawList and drawButtonMenu signatures are updated to accept UIIcon-based icon callbacks. Default parameters maintain backward compatibility.

Also applies to: 128-130

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

186-190: LGTM! Updated drawList signature accepts UIIcon callback.

The BaseTheme implementation intentionally does not render icons (classic theme behavior). Derived themes like LyraTheme override this method to render icons. The signature change maintains API consistency across themes.


636-638: LGTM! Updated drawButtonMenu signature accepts UIIcon callback.

Similar to drawList, the base implementation doesn't render icons but accepts the parameter for interface consistency with derived themes.

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

247-251: LGTM! Icon rendering in drawList is well-implemented.

The icon size adapts based on subtitle presence (32px for rows with subtitles, 24px otherwise), and layout calculations properly account for icon width. The icon is correctly fetched via iconForName and rendered with drawIcon.

Also applies to: 280-317


503-534: LGTM! drawButtonMenu correctly handles icon rendering.

The previous review concerns have been addressed:

  1. The label string is now stored in labelStr (line 518), preventing the dangling pointer issue.
  2. The rowIcon parameter is now properly used via rowIcon(i) to determine which icon to render (lines 525-526).

44-83: > Likely an incorrect or invalid review comment.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/UITheme.h`:
- Line 25: UITheme::getFileIcon currently calls filename.back() without checking
for an empty string; add an early guard at the start of UITheme::getFileIcon to
detect filename.empty() and return a safe default UIIcon (e.g.,
UIIcon::GenericFile or UIIcon::Unknown) instead of accessing filename.back(),
ensuring the public API doesn’t UB on empty inputs.

---

Duplicate comments:
In `@src/components/themes/BaseTheme.cpp`:
- Line 22: The hard-coded subtitleY = 738 in BaseTheme.cpp causes layout
breakage on different screen sizes; replace this magic constant with a dynamic
calculation (e.g., compute subtitleY from the current viewport or container
height and the header/footer metrics) inside the BaseTheme code path that
positions subtitles—use existing APIs or members that provide screen size or
header height (e.g., a getScreenHeight(), theme metrics, or Header::height())
and compute subtitleY = headerHeight + desiredOffset (or a percentage of the
screen) so subtitle positioning adapts to varying screen sizes.

---

Nitpick comments:
In `@src/components/themes/BaseTheme.h`:
- Around line 64-65: Change the plain enum UIIcon to a scoped enum (enum class
UIIcon) to prevent polluting the enclosing namespace; update all call sites to
use the scoped qualifiers (e.g., UIIcon::File, UIIcon::Text, UIIcon::Image,
etc.), including usage in LyraTheme.cpp and any switch/statements or variable
declarations that currently reference unqualified enumerators, and ensure any
implicit conversions to/from integers are made explicit where needed.

In `@src/components/themes/lyra/LyraTheme.cpp`:
- Around line 451-459: The drawIcon call in the hasCover false branch uses the
magic number 24 for X/Y offsets (renderer.drawIcon(CoverIcon, ... + 24, ... +
24, 32, 32)); introduce a named constant (e.g., constexpr int coverIconOffset =
24) in the Lyra theme source or header shared with Lyra3CoversTheme, replace the
hardcoded 24s with coverIconOffset in LyraTheme.cpp (and update
Lyra3CoversTheme.cpp line 75 to use the same constant) so positioning is
consistent and maintainable while leaving the existing renderer.drawIcon and
CoverIcon usage unchanged.
- Around line 39-42: The constant mainMenuColumns in LyraTheme.cpp is unused;
either remove the declaration constexpr int mainMenuColumns = 2 from
LyraTheme.cpp, or if it’s intended for future layout logic, keep it but add a
one-line comment above the declaration explaining its intended purpose (e.g.,
"number of columns for main menu layout - reserved for future use") so its
presence is intentional and clear; update any relevant unit tests or style
checks if they expect no unused symbols.

In `@src/components/UITheme.h`:
- Line 25: Change the hot-path function UIIcon getFileIcon(std::string filename)
to accept a const reference: UIIcon getFileIcon(const std::string& filename);
update the corresponding definition implementation (the function with the same
name) to match the new signature and ensure all callers (e.g.,
RecentBooksActivity::render and MyLibraryActivity::render) continue to compile;
this avoids unnecessary copies on each render while preserving const-correctness
since the function only reads filename.

static int getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints,
bool hasSubtitle);
static std::string getCoverThumbPath(std::string coverBmpPath, int coverHeight);
static UIIcon getFileIcon(std::string filename);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the getFileIcon implementation in UITheme.cpp
rg -n 'getFileIcon' src/components/UITheme.cpp -A 15

Repository: crosspoint-reader/crosspoint-reader

Length of output: 657


🏁 Script executed:

#!/bin/bash
# Find all call sites of getFileIcon
rg -n '\bgetFileIcon\s*\(' --type cpp --type h -B 2 -A 2

Repository: crosspoint-reader/crosspoint-reader

Length of output: 1714


🏁 Script executed:

#!/bin/bash
# Check RecentBooksActivity to understand recentBooks source
rg -n 'recentBooks' src/activities/home/RecentBooksActivity.cpp -B 5 -A 1 | head -40

Repository: crosspoint-reader/crosspoint-reader

Length of output: 1311


🏁 Script executed:

#!/bin/bash
# Check MyLibraryActivity to understand files source
rg -n '\bfiles\b' src/activities/home/MyLibraryActivity.cpp -B 3 -A 1 | grep -A 3 -B 3 'getFileIcon\|files ='

Repository: crosspoint-reader/crosspoint-reader

Length of output: 372


🏁 Script executed:

#!/bin/bash
# Check where 'files' variable is populated in MyLibraryActivity
rg -n 'files\s*=' src/activities/home/MyLibraryActivity.cpp -B 3 -A 3

Repository: crosspoint-reader/crosspoint-reader

Length of output: 61


🏁 Script executed:

#!/bin/bash
# Check the type of files variable in MyLibraryActivity
rg -n 'std::vector|files' src/activities/home/MyLibraryActivity.h -B 2 -A 2

Repository: crosspoint-reader/crosspoint-reader

Length of output: 190


🏁 Script executed:

#!/bin/bash
# Find where files vector is populated in MyLibraryActivity.cpp
rg -n 'files\.push_back\|files\.emplace_back\|\.listDir\|Storage' src/activities/home/MyLibraryActivity.cpp -B 2 -A 2

Repository: crosspoint-reader/crosspoint-reader

Length of output: 61


🏁 Script executed:

#!/bin/bash
# Search for file population logic in MyLibraryActivity.cpp
rg -n 'files' src/activities/home/MyLibraryActivity.cpp -B 2 -A 2 | head -60

Repository: crosspoint-reader/crosspoint-reader

Length of output: 1988


🏁 Script executed:

#!/bin/bash
# Check if there are any other potential sources for getFileIcon calls
# Look for any dynamic construction or user input paths
rg -n 'getFileIcon' --type cpp --type h

Repository: crosspoint-reader/crosspoint-reader

Length of output: 494


Add defensive guard for empty filename to prevent undefined behavior.

UITheme::getFileIcon at line 78 of src/components/UITheme.cpp calls filename.back() without checking if the string is empty. While current call sites only pass valid file system paths from directory listings, the public API does not enforce this contract. Add a guard to safely handle empty inputs:

 UIIcon UITheme::getFileIcon(std::string filename) {
+  if (filename.empty()) {
+    return File;
+  }
   if (filename.back() == '/') {
     return Folder;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/UITheme.h` at line 25, UITheme::getFileIcon currently calls
filename.back() without checking for an empty string; add an early guard at the
start of UITheme::getFileIcon to detect filename.empty() and return a safe
default UIIcon (e.g., UIIcon::GenericFile or UIIcon::Unknown) instead of
accessing filename.back(), ensuring the public API doesn’t UB on empty inputs.

daveallie
daveallie previously approved these changes Feb 19, 2026
@daveallie daveallie merged commit fdcd71e into crosspoint-reader:master Feb 19, 2026
5 checks passed
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.

🧹 Nitpick comments (1)
src/components/themes/lyra/LyraTheme.cpp (1)

503-533: Consider truncating menu labels after adding icons.
Icons reduce horizontal space, so long labels can clip or crowd. Truncating to the remaining width keeps layout stable.

♻️ Suggested adjustment
-    const char* label = labelStr.c_str();
     int textX = tileRect.x + 16;
     const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
     const int textY = tileRect.y + (LyraMetrics::values.menuRowHeight - lineHeight) / 2;

     if (rowIcon != nullptr) {
       UIIcon icon = rowIcon(i);
       const uint8_t* iconBitmap = iconForName(icon, mainMenuIconSize);
       if (iconBitmap != nullptr) {
         renderer.drawIcon(iconBitmap, textX, textY + 3, mainMenuIconSize, mainMenuIconSize);
         textX += mainMenuIconSize + hPaddingInSelection + 2;
       }
     }

-    renderer.drawText(UI_12_FONT_ID, textX, textY, label, true);
+    const int maxLabelWidth = tileRect.width - (textX - tileRect.x) - hPaddingInSelection;
+    auto truncatedLabel =
+        renderer.truncatedText(UI_12_FONT_ID, labelStr.c_str(), maxLabelWidth, EpdFontFamily::REGULAR);
+    renderer.drawText(UI_12_FONT_ID, textX, textY, truncatedLabel.c_str(), true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/themes/lyra/LyraTheme.cpp` around lines 503 - 533, In
LyraTheme::drawButtonMenu the label can overflow when an icon is drawn; compute
the available width for text inside tileRect after accounting for initial textX,
mainMenuIconSize and hPaddingInSelection, then measure and truncate the string
from buttonLabel(i) (append ellipsis) so renderer.drawText(UI_12_FONT_ID, textX,
textY, ...) is passed a label that fits; use renderer text-measure API (e.g.,
getTextWidth or getSubstringWidth) to iteratively trim or binary-search the
maximum characters that fit the available width before calling drawText.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ebfea9 and d325a1e.

📒 Files selected for processing (1)
  • src/components/themes/lyra/LyraTheme.cpp
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/themes/lyra/LyraTheme.cpp (1)
src/components/themes/BaseTheme.cpp (2)
  • drawList (186-254)
  • drawList (186-190)
⏰ 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 comments (4)
src/components/themes/lyra/LyraTheme.cpp (4)

13-83: Icon includes and UIIcon size mapping look consistent.


184-184: Line-end adjustments for header, subheader, and tab bar look good.

Also applies to: 212-212, 244-244


247-337: List icon integration aligns cleanly with UIIcon-based rendering.


452-458: Empty-cover fallback rendering is clear and self-contained.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/components/themes/lyra/LyraTheme.cpp`:
- Around line 503-533: In LyraTheme::drawButtonMenu the label can overflow when
an icon is drawn; compute the available width for text inside tileRect after
accounting for initial textX, mainMenuIconSize and hPaddingInSelection, then
measure and truncate the string from buttonLabel(i) (append ellipsis) so
renderer.drawText(UI_12_FONT_ID, textX, textY, ...) is passed a label that fits;
use renderer text-measure API (e.g., getTextWidth or getSubstringWidth) to
iteratively trim or binary-search the maximum characters that fit the available
width before calling drawText.

@pablohc
Copy link
Contributor

pablohc commented Feb 19, 2026

They look incredible!! Thanks @CaptainFrito

saslv pushed a commit to saslv/crosspoint-reader that referenced this pull request Feb 19, 2026
/!\ This PR depends on
crosspoint-reader#732 being
merged first

Also requires the
open-x4-epaper/community-sdk#18 PR

## Summary

Lyra theme icons on the home menu, in the file browser and on empty book
covers

![IMG_8023
Medium](https://github.com/user-attachments/assets/ba7c1407-94d2-4353-80ff-d5b800c6ac5b)
![IMG_8024
Medium](https://github.com/user-attachments/assets/edb59e13-b1c9-4c86-bef3-c61cc8134e64)
![IMG_7958
Medium](https://github.com/user-attachments/assets/d3079ce1-95f0-43f4-bbc7-1f747cc70203)
![IMG_8033
Medium](https://github.com/user-attachments/assets/f3e2e03b-0fa8-47b7-8717-c0b71361b7a8)


## Additional Context

- Added a function to the open-x4-sdk renderer to draw transparent
images
- Added a scripts/convert_icon.py script to convert svg/png icons into a
C array that can be directly imported into the project. Usage:
```bash
python ./scripts/convert_icon.py 'path/to/icon.png' cover 32 32
```
This will create a components/icons/cover.h file with a C array called
CoverIcon, of size 32x32px. Lyra uses icons from
https://lucide.dev/icons with a stroke width of 2px, that can be
downloaded with any desired size on the site.

> The file browser is noticeably slower with the addition of icons, and
using an image buffer like on the home page doesn't help very much. Any
suggestions to optimize this are welcome.

---

### 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? _**PARTIALLY**_
The icon conversion python script was generated by Copilot as I am not a
python dev.

---------

Co-authored-by: Dave Allie <[email protected]>
el pushed a commit to el/crosspoint-reader that referenced this pull request Feb 19, 2026
/!\ This PR depends on
crosspoint-reader#732 being
merged first

Also requires the
open-x4-epaper/community-sdk#18 PR

## Summary

Lyra theme icons on the home menu, in the file browser and on empty book
covers

![IMG_8023
Medium](https://github.com/user-attachments/assets/ba7c1407-94d2-4353-80ff-d5b800c6ac5b)
![IMG_8024
Medium](https://github.com/user-attachments/assets/edb59e13-b1c9-4c86-bef3-c61cc8134e64)
![IMG_7958
Medium](https://github.com/user-attachments/assets/d3079ce1-95f0-43f4-bbc7-1f747cc70203)
![IMG_8033
Medium](https://github.com/user-attachments/assets/f3e2e03b-0fa8-47b7-8717-c0b71361b7a8)


## Additional Context

- Added a function to the open-x4-sdk renderer to draw transparent
images
- Added a scripts/convert_icon.py script to convert svg/png icons into a
C array that can be directly imported into the project. Usage:
```bash
python ./scripts/convert_icon.py 'path/to/icon.png' cover 32 32
```
This will create a components/icons/cover.h file with a C array called
CoverIcon, of size 32x32px. Lyra uses icons from
https://lucide.dev/icons with a stroke width of 2px, that can be
downloaded with any desired size on the site.

> The file browser is noticeably slower with the addition of icons, and
using an image buffer like on the home page doesn't help very much. Any
suggestions to optimize this are welcome.

---

### 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? _**PARTIALLY**_
The icon conversion python script was generated by Copilot as I am not a
python dev.

---------

Co-authored-by: Dave Allie <[email protected]>
lukestein pushed a commit to lukestein/crosspoint-reader that referenced this pull request Feb 20, 2026
/!\ This PR depends on
crosspoint-reader#732 being
merged first

Also requires the
open-x4-epaper/community-sdk#18 PR

## Summary

Lyra theme icons on the home menu, in the file browser and on empty book
covers

![IMG_8023
Medium](https://github.com/user-attachments/assets/ba7c1407-94d2-4353-80ff-d5b800c6ac5b)
![IMG_8024
Medium](https://github.com/user-attachments/assets/edb59e13-b1c9-4c86-bef3-c61cc8134e64)
![IMG_7958
Medium](https://github.com/user-attachments/assets/d3079ce1-95f0-43f4-bbc7-1f747cc70203)
![IMG_8033
Medium](https://github.com/user-attachments/assets/f3e2e03b-0fa8-47b7-8717-c0b71361b7a8)


## Additional Context

- Added a function to the open-x4-sdk renderer to draw transparent
images
- Added a scripts/convert_icon.py script to convert svg/png icons into a
C array that can be directly imported into the project. Usage:
```bash
python ./scripts/convert_icon.py 'path/to/icon.png' cover 32 32
```
This will create a components/icons/cover.h file with a C array called
CoverIcon, of size 32x32px. Lyra uses icons from
https://lucide.dev/icons with a stroke width of 2px, that can be
downloaded with any desired size on the site.

> The file browser is noticeably slower with the addition of icons, and
using an image buffer like on the home page doesn't help very much. Any
suggestions to optimize this are welcome.

---

### 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? _**PARTIALLY**_
The icon conversion python script was generated by Copilot as I am not a
python dev.

---------

Co-authored-by: Dave Allie <[email protected]>
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.

10 participants