feat: Lyra screens (#732)
Implements Lyra theme for some more Crosspoint screens:       - A bit of refactoring for list scrolling logic --- 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? _**NO**_ --------- Co-authored-by: Dave Allie <dave@daveallie.com>
This commit is contained in:
@@ -23,6 +23,10 @@ constexpr int batteryPercentSpacing = 4;
|
||||
constexpr int hPaddingInSelection = 8;
|
||||
constexpr int cornerRadius = 6;
|
||||
constexpr int topHintButtonY = 345;
|
||||
constexpr int popupMarginX = 16;
|
||||
constexpr int popupMarginY = 12;
|
||||
constexpr int maxSubtitleWidth = 100;
|
||||
constexpr int maxListValueWidth = 200;
|
||||
} // namespace
|
||||
|
||||
void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
|
||||
@@ -104,7 +108,7 @@ void LyraTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const b
|
||||
}
|
||||
}
|
||||
|
||||
void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const {
|
||||
void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const {
|
||||
renderer.fillRect(rect.x, rect.y, rect.width, rect.height, false);
|
||||
|
||||
const bool showBatteryPercentage =
|
||||
@@ -135,14 +139,43 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
||||
}
|
||||
}
|
||||
|
||||
int maxTitleWidth =
|
||||
rect.width - LyraMetrics::values.contentSidePadding * 2 - (subtitle != nullptr ? maxSubtitleWidth : 0);
|
||||
|
||||
if (title) {
|
||||
auto truncatedTitle = renderer.truncatedText(
|
||||
UI_12_FONT_ID, title, rect.width - LyraMetrics::values.contentSidePadding * 2, EpdFontFamily::BOLD);
|
||||
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title, maxTitleWidth, EpdFontFamily::BOLD);
|
||||
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);
|
||||
}
|
||||
|
||||
if (subtitle) {
|
||||
auto truncatedSubtitle = renderer.truncatedText(SMALL_FONT_ID, subtitle, maxSubtitleWidth, EpdFontFamily::REGULAR);
|
||||
int truncatedSubtitleWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedSubtitle.c_str());
|
||||
renderer.drawText(SMALL_FONT_ID,
|
||||
rect.x + rect.width - LyraMetrics::values.contentSidePadding - truncatedSubtitleWidth,
|
||||
rect.y + 50, truncatedSubtitle.c_str(), true);
|
||||
}
|
||||
}
|
||||
|
||||
void LyraTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, const char* rightLabel) const {
|
||||
int currentX = rect.x + LyraMetrics::values.contentSidePadding;
|
||||
int rightSpace = LyraMetrics::values.contentSidePadding;
|
||||
if (rightLabel) {
|
||||
auto truncatedRightLabel =
|
||||
renderer.truncatedText(SMALL_FONT_ID, rightLabel, maxListValueWidth, EpdFontFamily::REGULAR);
|
||||
int rightLabelWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedRightLabel.c_str());
|
||||
renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - LyraMetrics::values.contentSidePadding - rightLabelWidth,
|
||||
rect.y + 7, truncatedRightLabel.c_str());
|
||||
rightSpace += rightLabelWidth + hPaddingInSelection;
|
||||
}
|
||||
|
||||
auto truncatedLabel = renderer.truncatedText(
|
||||
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);
|
||||
}
|
||||
|
||||
void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs,
|
||||
@@ -181,7 +214,7 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
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) const {
|
||||
const std::function<std::string(int index)>& rowValue, bool highlightValue) const {
|
||||
int rowHeight =
|
||||
(rowSubtitle != nullptr) ? LyraMetrics::values.listWithSubtitleRowHeight : LyraMetrics::values.listRowHeight;
|
||||
int pageItems = rect.height / rowHeight;
|
||||
@@ -216,8 +249,14 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
const int itemY = rect.y + (i % pageItems) * rowHeight;
|
||||
|
||||
// Draw name
|
||||
int textWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2 -
|
||||
(rowValue != nullptr ? 60 : 0); // TODO truncate according to value width?
|
||||
int valueWidth = 0;
|
||||
std::string valueText = "";
|
||||
if (rowValue != nullptr) {
|
||||
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;
|
||||
}
|
||||
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,
|
||||
@@ -231,22 +270,16 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
itemY + 30, subtitle.c_str(), true);
|
||||
}
|
||||
|
||||
if (rowValue != nullptr) {
|
||||
// Draw value
|
||||
std::string valueText = rowValue(i);
|
||||
if (!valueText.empty()) {
|
||||
const auto valueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
|
||||
|
||||
if (i == selectedIndex) {
|
||||
renderer.fillRoundedRect(
|
||||
contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection * 2 - valueTextWidth, itemY,
|
||||
valueTextWidth + hPaddingInSelection * 2, rowHeight, cornerRadius, Color::Black);
|
||||
}
|
||||
|
||||
renderer.drawText(UI_10_FONT_ID,
|
||||
contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - valueTextWidth,
|
||||
itemY + 6, valueText.c_str(), i != selectedIndex);
|
||||
// Draw value
|
||||
if (!valueText.empty()) {
|
||||
if (i == selectedIndex && highlightValue) {
|
||||
renderer.fillRoundedRect(
|
||||
contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - valueWidth, itemY,
|
||||
valueWidth + hPaddingInSelection, rowHeight, cornerRadius, Color::Black);
|
||||
}
|
||||
|
||||
renderer.drawText(UI_10_FONT_ID, rect.x + contentWidth - LyraMetrics::values.contentSidePadding - valueWidth,
|
||||
itemY + 6, valueText.c_str(), !(i == selectedIndex && highlightValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,12 +364,10 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
const int coverHeight = LyraMetrics::values.homeCoverHeight;
|
||||
|
||||
if (bookCount == 0) {
|
||||
const int centerY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2;
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, centerY, tr(STR_CHOOSE_SOMETHING), true);
|
||||
drawEmptyRecents(renderer, rect);
|
||||
return;
|
||||
}
|
||||
|
||||
// Word-wrap helper: splits text into lines fitting maxWidth, capped at maxLines with ellipsis
|
||||
auto wrapText = [&renderer](int fontId, const std::string& text, int maxWidth,
|
||||
int maxLines) -> std::vector<std::string> {
|
||||
std::vector<std::string> words;
|
||||
@@ -390,8 +421,6 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
return lines;
|
||||
};
|
||||
|
||||
// Cover rendering helper: draws bitmap maintaining aspect ratio within a slot.
|
||||
// Crops if wider than slot, centers if narrower. Returns actual rendered width.
|
||||
auto& storage = HalStorage::getInstance();
|
||||
auto renderCoverBitmap = [&renderer, &storage, coverHeight](const std::string& coverBmpPath, int slotX, int slotY,
|
||||
int slotWidth) {
|
||||
@@ -418,11 +447,9 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
};
|
||||
|
||||
if (bookCount == 1) {
|
||||
// ===== SINGLE BOOK: HORIZONTAL LAYOUT (cover left, text right) =====
|
||||
const bool bookSelected = (selectorIndex == 0);
|
||||
const int cardX = LyraMetrics::values.contentSidePadding;
|
||||
const int cardWidth = rect.width - 2 * LyraMetrics::values.contentSidePadding;
|
||||
// Fixed cover slot width based on typical book aspect ratio (~0.65)
|
||||
const int coverSlotWidth = static_cast<int>(coverHeight * 0.65f);
|
||||
const int textGap = hPaddingInSelection * 2;
|
||||
const int textAreaX = cardX + hPaddingInSelection + coverSlotWidth + textGap;
|
||||
@@ -439,20 +466,14 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
coverRendered = true;
|
||||
}
|
||||
|
||||
// Selection highlight: border strips around the cover, fill the text area
|
||||
if (bookSelected) {
|
||||
// Top strip
|
||||
renderer.fillRoundedRect(cardX, tileY, cardWidth, hPaddingInSelection, cornerRadius, true, true, false, false,
|
||||
Color::LightGray);
|
||||
// Left strip (alongside cover)
|
||||
renderer.fillRectDither(cardX, tileY + hPaddingInSelection, hPaddingInSelection, coverHeight, Color::LightGray);
|
||||
// Right strip
|
||||
renderer.fillRectDither(cardX + cardWidth - hPaddingInSelection, tileY + hPaddingInSelection, hPaddingInSelection,
|
||||
coverHeight, Color::LightGray);
|
||||
// Text area background (right of cover, alongside cover height)
|
||||
renderer.fillRectDither(cardX + hPaddingInSelection + coverSlotWidth, tileY + hPaddingInSelection,
|
||||
cardWidth - hPaddingInSelection * 2 - coverSlotWidth, coverHeight, Color::LightGray);
|
||||
// Bottom strip (below cover, full width)
|
||||
const int bottomY = tileY + hPaddingInSelection + coverHeight;
|
||||
const int bottomH = tileHeight - hPaddingInSelection - coverHeight;
|
||||
if (bottomH > 0) {
|
||||
@@ -461,7 +482,6 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
}
|
||||
}
|
||||
|
||||
// Title: UI_12 font, wrap generously (up to 5 lines)
|
||||
auto titleLines = wrapText(UI_12_FONT_ID, recentBooks[0].title, textAreaWidth, 5);
|
||||
const int titleLineHeight = renderer.getLineHeight(UI_12_FONT_ID);
|
||||
int textY = tileY + hPaddingInSelection + 3;
|
||||
@@ -470,7 +490,6 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
textY += titleLineHeight;
|
||||
}
|
||||
|
||||
// Author: UI_10 font
|
||||
if (!recentBooks[0].author.empty()) {
|
||||
textY += 4;
|
||||
auto author = renderer.truncatedText(UI_10_FONT_ID, recentBooks[0].author.c_str(), textAreaWidth);
|
||||
@@ -478,12 +497,9 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
}
|
||||
|
||||
} else {
|
||||
// ===== MULTI BOOK: TILE LAYOUT (2-3 books) =====
|
||||
const int tileWidth = (rect.width - 2 * LyraMetrics::values.contentSidePadding) / bookCount;
|
||||
// Bottom section height: everything below cover + top padding
|
||||
const int bottomSectionHeight = tileHeight - coverHeight - hPaddingInSelection;
|
||||
|
||||
// Render covers (first render only)
|
||||
if (!coverRendered) {
|
||||
for (int i = 0; i < bookCount; i++) {
|
||||
int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i;
|
||||
@@ -498,27 +514,22 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
coverRendered = true;
|
||||
}
|
||||
|
||||
// Draw selection and text for each book tile
|
||||
for (int i = 0; i < bookCount; i++) {
|
||||
bool bookSelected = (selectorIndex == i);
|
||||
int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i;
|
||||
const int maxTextWidth = tileWidth - 2 * hPaddingInSelection;
|
||||
|
||||
if (bookSelected) {
|
||||
// Top strip
|
||||
renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false,
|
||||
Color::LightGray);
|
||||
// Left/right strips alongside cover
|
||||
renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection, coverHeight,
|
||||
Color::LightGray);
|
||||
renderer.fillRectDither(tileX + tileWidth - hPaddingInSelection, tileY + hPaddingInSelection,
|
||||
hPaddingInSelection, coverHeight, Color::LightGray);
|
||||
// Bottom section: spans from below cover to the card bottom
|
||||
renderer.fillRoundedRect(tileX, tileY + coverHeight + hPaddingInSelection, tileWidth, bottomSectionHeight,
|
||||
cornerRadius, false, false, true, true, Color::LightGray);
|
||||
}
|
||||
|
||||
// Word-wrap title to 2 lines (UI_10)
|
||||
auto titleLines = wrapText(UI_10_FONT_ID, recentBooks[i].title, maxTextWidth, 2);
|
||||
const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
|
||||
|
||||
@@ -528,7 +539,6 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
textY += lineHeight;
|
||||
}
|
||||
|
||||
// Author below title
|
||||
if (!recentBooks[i].author.empty()) {
|
||||
auto author = renderer.truncatedText(SMALL_FONT_ID, recentBooks[i].author.c_str(), maxTextWidth);
|
||||
renderer.drawText(SMALL_FONT_ID, tileX + hPaddingInSelection, textY + 2, author.c_str(), true);
|
||||
@@ -537,15 +547,22 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
}
|
||||
}
|
||||
|
||||
void LyraTheme::drawEmptyRecents(const GfxRenderer& renderer, const Rect rect) const {
|
||||
constexpr int padding = 48;
|
||||
renderer.drawText(UI_12_FONT_ID, rect.x + padding,
|
||||
rect.y + rect.height / 2 - renderer.getLineHeight(UI_12_FONT_ID) - 2, tr(STR_NO_OPEN_BOOK), true,
|
||||
EpdFontFamily::BOLD);
|
||||
renderer.drawText(UI_10_FONT_ID, rect.x + padding, rect.y + rect.height / 2 + 2, tr(STR_START_READING), true);
|
||||
}
|
||||
|
||||
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 {
|
||||
for (int i = 0; i < buttonCount; ++i) {
|
||||
int tileWidth = (rect.width - LyraMetrics::values.contentSidePadding * 2 - LyraMetrics::values.menuSpacing) / 2;
|
||||
Rect tileRect =
|
||||
Rect{rect.x + LyraMetrics::values.contentSidePadding + (LyraMetrics::values.menuSpacing + tileWidth) * (i % 2),
|
||||
rect.y + static_cast<int>(i / 2) * (LyraMetrics::values.menuRowHeight + LyraMetrics::values.menuSpacing),
|
||||
tileWidth, LyraMetrics::values.menuRowHeight};
|
||||
int tileWidth = rect.width - LyraMetrics::values.contentSidePadding * 2;
|
||||
Rect tileRect = Rect{rect.x + LyraMetrics::values.contentSidePadding,
|
||||
rect.y + i * (LyraMetrics::values.menuRowHeight + LyraMetrics::values.menuSpacing), tileWidth,
|
||||
LyraMetrics::values.menuRowHeight};
|
||||
|
||||
const bool selected = selectedIndex == i;
|
||||
|
||||
@@ -581,4 +598,36 @@ Rect LyraTheme::drawPopup(const GfxRenderer& renderer, const char* message) cons
|
||||
renderer.drawText(UI_12_FONT_ID, textX, textY, message, true, EpdFontFamily::REGULAR);
|
||||
renderer.displayBuffer();
|
||||
return Rect{x, y, w, h};
|
||||
}
|
||||
}
|
||||
|
||||
void LyraTheme::fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const {
|
||||
constexpr int barHeight = 4;
|
||||
|
||||
const int barWidth = layout.width - popupMarginX * 2;
|
||||
const int barX = layout.x + (layout.width - barWidth) / 2;
|
||||
const int barY = layout.y + layout.height - popupMarginY / 2 - barHeight / 2 - 1;
|
||||
|
||||
int fillWidth = barWidth * progress / 100;
|
||||
|
||||
renderer.fillRect(barX, barY, fillWidth, barHeight, false);
|
||||
|
||||
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
||||
}
|
||||
|
||||
void LyraTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const {
|
||||
int lineY = rect.y + rect.height + renderer.getLineHeight(UI_12_FONT_ID) + LyraMetrics::values.verticalSpacing;
|
||||
int lineW = textWidth + hPaddingInSelection * 2;
|
||||
renderer.drawLine(rect.x + (rect.width - lineW) / 2, lineY, rect.x + (rect.width + lineW) / 2, lineY, 3);
|
||||
}
|
||||
|
||||
void LyraTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label,
|
||||
const bool isSelected) const {
|
||||
if (isSelected) {
|
||||
renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cornerRadius, Color::Black);
|
||||
}
|
||||
|
||||
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, label);
|
||||
const int textX = rect.x + (rect.width - textWidth) / 2;
|
||||
const int textY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2;
|
||||
renderer.drawText(UI_12_FONT_ID, textX, textY, label, !isSelected);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user