feat: Lyra Icons (#725)

/!\ This PR depends on
https://github.com/crosspoint-reader/crosspoint-reader/pull/732 being
merged first

Also requires the
https://github.com/open-x4-epaper/community-sdk/pull/18 PR

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)

- 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.

---

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 <dave@daveallie.com>
This commit is contained in:
CaptainFrito
2026-02-19 17:38:09 +07:00
committed by cottongin
parent 724c1969b9
commit a8a89e35b8
30 changed files with 419 additions and 29 deletions

View File

@@ -188,7 +188,7 @@ void BaseTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* top
void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
const std::function<std::string(int index)>& rowTitle,
const std::function<std::string(int index)>& rowSubtitle,
const std::function<std::string(int index)>& rowIcon,
const std::function<UIIcon(int index)>& rowIcon,
const std::function<std::string(int index)>& rowValue, bool highlightValue) const {
int rowHeight =
(rowSubtitle != nullptr) ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight;
@@ -657,7 +657,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
void BaseTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex,
const std::function<std::string(int index)>& buttonLabel,
const std::function<std::string(int index)>& rowIcon) const {
const std::function<UIIcon(int index)>& rowIcon) const {
for (int i = 0; i < buttonCount; ++i) {
const int tileY = BaseMetrics::values.verticalSpacing + rect.y +
static_cast<int>(i) * (BaseMetrics::values.menuRowHeight + BaseMetrics::values.menuSpacing);
@@ -749,4 +749,4 @@ void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const ch
renderer.drawText(UI_10_FONT_ID, textX + itemWidth, rect.y, "]");
}
renderer.drawText(UI_10_FONT_ID, textX, rect.y, label);
}
}

View File

@@ -61,6 +61,8 @@ struct ThemeMetrics {
bool keyboardCenteredText;
};
enum UIIcon { Folder, Text, Image, Book, File, Recent, Settings, Transfer, Library, Wifi, Hotspot };
// Default theme implementation (Classic Theme)
// Additional themes can inherit from this and override methods as needed
@@ -111,7 +113,7 @@ class BaseTheme {
virtual void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
const std::function<std::string(int index)>& rowTitle,
const std::function<std::string(int index)>& rowSubtitle = nullptr,
const std::function<std::string(int index)>& rowIcon = nullptr,
const std::function<UIIcon(int index)>& rowIcon = nullptr,
const std::function<std::string(int index)>& rowValue = nullptr,
bool highlightValue = false) const;
virtual void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title,
@@ -125,11 +127,11 @@ class BaseTheme {
bool& bufferRestored, std::function<bool()> storeCoverBuffer) const;
virtual void drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex,
const std::function<std::string(int index)>& buttonLabel,
const std::function<std::string(int index)>& rowIcon) const;
const std::function<UIIcon(int index)>& rowIcon) const;
virtual Rect drawPopup(const GfxRenderer& renderer, const char* message) const;
virtual void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const;
virtual void drawReadingProgressBar(const GfxRenderer& renderer, const size_t bookProgress) const;
virtual void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const;
virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const;
virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const;
};
};

View File

@@ -14,8 +14,21 @@
#include "CrossPointSettings.h"
#include "RecentBooksStore.h"
#include "components/UITheme.h"
#include "components/icons/book.h"
#include "components/icons/book24.h"
#include "components/icons/cover.h"
#include "components/icons/file24.h"
#include "components/icons/folder.h"
#include "components/icons/folder24.h"
#include "components/icons/hotspot.h"
#include "components/icons/image24.h"
#include "components/icons/library.h"
#include "components/icons/recent.h"
#include "components/icons/settings2.h"
#include "components/icons/text24.h"
#include "components/icons/transfer.h"
#include "components/icons/wifi.h"
#include "fontIds.h"
#include "util/StringUtils.h"
// Internal constants
namespace {
@@ -27,6 +40,50 @@ constexpr int popupMarginX = 16;
constexpr int popupMarginY = 12;
constexpr int maxSubtitleWidth = 100;
constexpr int maxListValueWidth = 200;
constexpr int mainMenuIconSize = 32;
constexpr int listIconSize = 24;
constexpr int mainMenuColumns = 2;
const uint8_t* iconForName(UIIcon icon, int size) {
if (size == 24) {
switch (icon) {
case UIIcon::Folder:
return Folder24Icon;
case UIIcon::Text:
return Text24Icon;
case UIIcon::Image:
return Image24Icon;
case UIIcon::Book:
return Book24Icon;
case UIIcon::File:
return File24Icon;
default:
return nullptr;
}
} else if (size == 32) {
switch (icon) {
case UIIcon::Folder:
return FolderIcon;
case UIIcon::Book:
return BookIcon;
case UIIcon::Recent:
return RecentIcon;
case UIIcon::Settings:
return Settings2Icon;
case UIIcon::Transfer:
return TransferIcon;
case UIIcon::Library:
return LibraryIcon;
case UIIcon::Wifi:
return WifiIcon;
case UIIcon::Hotspot:
return HotspotIcon;
default:
return nullptr;
}
}
return nullptr;
}
} // namespace
void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
@@ -147,7 +204,7 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
renderer.drawText(UI_12_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding,
rect.y + LyraMetrics::values.batteryBarHeight + 3, truncatedTitle.c_str(), true,
EpdFontFamily::BOLD);
renderer.drawLine(rect.x, rect.y + rect.height - 3, rect.x + rect.width, rect.y + rect.height - 3, 3, true);
renderer.drawLine(rect.x, rect.y + rect.height - 3, rect.x + rect.width - 1, rect.y + rect.height - 3, 3, true);
}
if (subtitle) {
@@ -175,7 +232,7 @@ void LyraTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char
UI_10_FONT_ID, label, rect.width - LyraMetrics::values.contentSidePadding - rightSpace, EpdFontFamily::REGULAR);
renderer.drawText(UI_10_FONT_ID, currentX, rect.y + 6, truncatedLabel.c_str(), true, EpdFontFamily::REGULAR);
renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width, rect.y + rect.height - 1, true);
renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width - 1, rect.y + rect.height - 1, true);
}
void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs,
@@ -207,13 +264,13 @@ void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::ve
currentX += textWidth + LyraMetrics::values.tabSpacing + 2 * hPaddingInSelection;
}
renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width, rect.y + rect.height - 1, true);
renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width - 1, rect.y + rect.height - 1, true);
}
void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
const std::function<std::string(int index)>& rowTitle,
const std::function<std::string(int index)>& rowSubtitle,
const std::function<std::string(int index)>& rowIcon,
const std::function<UIIcon(int index)>& rowIcon,
const std::function<std::string(int index)>& rowValue, bool highlightValue) const {
int rowHeight =
(rowSubtitle != nullptr) ? LyraMetrics::values.listWithSubtitleRowHeight : LyraMetrics::values.listRowHeight;
@@ -243,10 +300,21 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
Color::LightGray);
}
int textX = rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection;
int textWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2;
int iconSize;
if (rowIcon != nullptr) {
iconSize = (rowSubtitle != nullptr) ? mainMenuIconSize : listIconSize;
textX += iconSize + hPaddingInSelection;
textWidth -= iconSize + hPaddingInSelection;
}
// Draw all items
const auto pageStartIndex = selectedIndex / pageItems * pageItems;
int iconY = (rowSubtitle != nullptr) ? 16 : 10;
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) {
const int itemY = rect.y + (i % pageItems) * rowHeight;
int rowTextWidth = textWidth;
// Draw name
int valueWidth = 0;
@@ -255,19 +323,27 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
valueText = rowValue(i);
valueText = renderer.truncatedText(UI_10_FONT_ID, valueText.c_str(), maxListValueWidth);
valueWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()) + hPaddingInSelection;
rowTextWidth -= valueWidth;
}
int textWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2 - valueWidth;
auto itemName = rowTitle(i);
auto item = renderer.truncatedText(UI_10_FONT_ID, itemName.c_str(), textWidth);
renderer.drawText(UI_10_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection * 2,
itemY + 6, item.c_str(), true);
auto item = renderer.truncatedText(UI_10_FONT_ID, itemName.c_str(), rowTextWidth);
renderer.drawText(UI_10_FONT_ID, textX, itemY + 7, item.c_str(), true);
if (rowIcon != nullptr) {
UIIcon icon = rowIcon(i);
const uint8_t* iconBitmap = iconForName(icon, iconSize);
if (iconBitmap != nullptr) {
renderer.drawIcon(iconBitmap, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection,
itemY + iconY, iconSize, iconSize);
}
}
if (rowSubtitle != nullptr) {
// Draw subtitle
std::string subtitleText = rowSubtitle(i);
auto subtitle = renderer.truncatedText(SMALL_FONT_ID, subtitleText.c_str(), textWidth);
renderer.drawText(SMALL_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection * 2,
itemY + 30, subtitle.c_str(), true);
auto subtitle = renderer.truncatedText(SMALL_FONT_ID, subtitleText.c_str(), rowTextWidth);
renderer.drawText(SMALL_FONT_ID, textX, itemY + 30, subtitle.c_str(), true);
}
// Draw value
@@ -557,7 +633,7 @@ void LyraTheme::drawEmptyRecents(const GfxRenderer& renderer, const Rect rect) c
void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex,
const std::function<std::string(int index)>& buttonLabel,
const std::function<std::string(int index)>& rowIcon) const {
const std::function<UIIcon(int index)>& rowIcon) const {
for (int i = 0; i < buttonCount; ++i) {
int tileWidth = rect.width - LyraMetrics::values.contentSidePadding * 2;
Rect tileRect = Rect{rect.x + LyraMetrics::values.contentSidePadding,
@@ -572,11 +648,19 @@ void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount
std::string labelStr = buttonLabel(i);
const char* label = labelStr.c_str();
const int textX = tileRect.x + 16;
int textX = tileRect.x + 16;
const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
const int textY = tileRect.y + (LyraMetrics::values.menuRowHeight - lineHeight) / 2;
// Invert text when the tile is selected, to contrast with the filled background
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);
}
}

View File

@@ -50,14 +50,14 @@ class LyraTheme : public BaseTheme {
void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
const std::function<std::string(int index)>& rowTitle,
const std::function<std::string(int index)>& rowSubtitle,
const std::function<std::string(int index)>& rowIcon,
const std::function<std::string(int index)>& rowValue, bool highlightValue) const override;
const std::function<UIIcon(int index)>& rowIcon, const std::function<std::string(int index)>& rowValue,
bool highlightValue) const override;
void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3,
const char* btn4) const override;
void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const override;
void drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex,
const std::function<std::string(int index)>& buttonLabel,
const std::function<std::string(int index)>& rowIcon) const override;
const std::function<UIIcon(int index)>& rowIcon) const override;
void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
const int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored,
std::function<bool()> storeCoverBuffer) const override;