#include "LyraTheme.h" #include #include #include #include #include #include #include #include #include "Battery.h" #include "CrossPointSettings.h" #include "RecentBooksStore.h" #include "components/UITheme.h" #include "fontIds.h" #include "util/StringUtils.h" // Internal constants namespace { constexpr int batteryPercentSpacing = 4; constexpr int hPaddingInSelection = 8; constexpr int cornerRadius = 6; constexpr int topHintButtonY = 345; } // namespace void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { // Left aligned: icon on left, percentage on right (reader mode) const uint16_t percentage = battery.readPercentage(); const int y = rect.y + 6; const int battWidth = LyraMetrics::values.batteryWidth; if (showPercentage) { const auto percentageText = std::to_string(percentage) + "%"; renderer.drawText(SMALL_FONT_ID, rect.x + batteryPercentSpacing + battWidth, rect.y, percentageText.c_str()); } // Draw icon const int x = rect.x; // Top line renderer.drawLine(x + 1, y, x + battWidth - 3, y); // Bottom line renderer.drawLine(x + 1, y + rect.height - 1, x + battWidth - 3, y + rect.height - 1); // Left line renderer.drawLine(x, y + 1, x, y + rect.height - 2); // Battery end renderer.drawLine(x + battWidth - 2, y + 1, x + battWidth - 2, y + rect.height - 2); renderer.drawPixel(x + battWidth - 1, y + 3); renderer.drawPixel(x + battWidth - 1, y + rect.height - 4); renderer.drawLine(x + battWidth - 0, y + 4, x + battWidth - 0, y + rect.height - 5); // Draw bars if (percentage > 10) { renderer.fillRect(x + 2, y + 2, 3, rect.height - 4); } if (percentage > 40) { renderer.fillRect(x + 6, y + 2, 3, rect.height - 4); } if (percentage > 70) { renderer.fillRect(x + 10, y + 2, 3, rect.height - 4); } } void LyraTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { // Right aligned: percentage on left, icon on right (UI headers) const uint16_t percentage = battery.readPercentage(); const int y = rect.y + 6; const int battWidth = LyraMetrics::values.batteryWidth; if (showPercentage) { const auto percentageText = std::to_string(percentage) + "%"; const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()); // Clear the area where we're going to draw the text to prevent ghosting const auto textHeight = renderer.getTextHeight(SMALL_FONT_ID); renderer.fillRect(rect.x - textWidth - batteryPercentSpacing, rect.y, textWidth, textHeight, false); // Draw text to the left of the icon renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - batteryPercentSpacing, rect.y, percentageText.c_str()); } // Draw icon at rect.x const int x = rect.x; // Top line renderer.drawLine(x + 1, y, x + battWidth - 3, y); // Bottom line renderer.drawLine(x + 1, y + rect.height - 1, x + battWidth - 3, y + rect.height - 1); // Left line renderer.drawLine(x, y + 1, x, y + rect.height - 2); // Battery end renderer.drawLine(x + battWidth - 2, y + 1, x + battWidth - 2, y + rect.height - 2); renderer.drawPixel(x + battWidth - 1, y + 3); renderer.drawPixel(x + battWidth - 1, y + rect.height - 4); renderer.drawLine(x + battWidth - 0, y + 4, x + battWidth - 0, y + rect.height - 5); // Draw bars if (percentage > 10) { renderer.fillRect(x + 2, y + 2, 3, rect.height - 4); } if (percentage > 40) { renderer.fillRect(x + 6, y + 2, 3, rect.height - 4); } if (percentage > 70) { renderer.fillRect(x + 10, y + 2, 3, rect.height - 4); } } void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const { renderer.fillRect(rect.x, rect.y, rect.width, rect.height, false); const bool showBatteryPercentage = SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS; // Position icon at right edge, drawBatteryRight will place text to the left const int batteryX = rect.x + rect.width - 12 - LyraMetrics::values.batteryWidth; drawBatteryRight(renderer, Rect{batteryX, rect.y + 5, LyraMetrics::values.batteryWidth, LyraMetrics::values.batteryHeight}, showBatteryPercentage); // Draw clock on the left side (symmetric with battery on the right) if (SETTINGS.clockFormat != CrossPointSettings::CLOCK_OFF) { time_t now = time(nullptr); struct tm* t = localtime(&now); if (t != nullptr && t->tm_year > 100) { char timeBuf[16]; if (SETTINGS.clockFormat == CrossPointSettings::CLOCK_24H) { snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d", t->tm_hour, t->tm_min); } else { int hour12 = t->tm_hour % 12; if (hour12 == 0) hour12 = 12; snprintf(timeBuf, sizeof(timeBuf), "%d:%02d %s", hour12, t->tm_min, t->tm_hour >= 12 ? "PM" : "AM"); } int clockFont = SMALL_FONT_ID; if (SETTINGS.clockSize == CrossPointSettings::CLOCK_SIZE_MEDIUM) clockFont = UI_10_FONT_ID; else if (SETTINGS.clockSize == CrossPointSettings::CLOCK_SIZE_LARGE) clockFont = UI_12_FONT_ID; renderer.drawText(clockFont, rect.x + 12, rect.y + 5, timeBuf, true); } } if (title) { auto truncatedTitle = renderer.truncatedText( UI_12_FONT_ID, title, rect.width - LyraMetrics::values.contentSidePadding * 2, 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); } } void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector& tabs, bool selected) const { int currentX = rect.x + LyraMetrics::values.contentSidePadding; if (selected) { renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, Color::LightGray); } for (const auto& tab : tabs) { const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, tab.label, EpdFontFamily::REGULAR); if (tab.selected) { if (selected) { renderer.fillRoundedRect(currentX, rect.y + 1, textWidth + 2 * hPaddingInSelection, rect.height - 4, cornerRadius, Color::Black); } else { renderer.fillRectDither(currentX, rect.y, textWidth + 2 * hPaddingInSelection, rect.height - 3, Color::LightGray); renderer.drawLine(currentX, rect.y + rect.height - 3, currentX + textWidth + 2 * hPaddingInSelection, rect.y + rect.height - 3, 2, true); } } renderer.drawText(UI_10_FONT_ID, currentX + hPaddingInSelection, rect.y + 6, tab.label, !(tab.selected && selected), EpdFontFamily::REGULAR); 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); } void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, const std::function& rowTitle, const std::function& rowSubtitle, const std::function& rowIcon, const std::function& rowValue) const { int rowHeight = (rowSubtitle != nullptr) ? LyraMetrics::values.listWithSubtitleRowHeight : LyraMetrics::values.listRowHeight; int pageItems = rect.height / rowHeight; const int totalPages = (itemCount + pageItems - 1) / pageItems; if (totalPages > 1) { const int scrollAreaHeight = rect.height; // Draw scroll bar const int scrollBarHeight = (scrollAreaHeight * pageItems) / itemCount; const int currentPage = selectedIndex / pageItems; const int scrollBarY = rect.y + ((scrollAreaHeight - scrollBarHeight) * currentPage) / (totalPages - 1); const int scrollBarX = rect.x + rect.width - LyraMetrics::values.scrollBarRightOffset; renderer.drawLine(scrollBarX, rect.y, scrollBarX, rect.y + scrollAreaHeight, true); renderer.fillRect(scrollBarX - LyraMetrics::values.scrollBarWidth, scrollBarY, LyraMetrics::values.scrollBarWidth, scrollBarHeight, true); } // Draw selection int contentWidth = rect.width - (totalPages > 1 ? (LyraMetrics::values.scrollBarWidth + LyraMetrics::values.scrollBarRightOffset) : 1); if (selectedIndex >= 0) { renderer.fillRoundedRect(LyraMetrics::values.contentSidePadding, rect.y + selectedIndex % pageItems * rowHeight, contentWidth - LyraMetrics::values.contentSidePadding * 2, rowHeight, cornerRadius, Color::LightGray); } // Draw all items const auto pageStartIndex = selectedIndex / pageItems * pageItems; for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) { 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? 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); 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); } 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); } } } } void LyraTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, const char* btn4) const { const GfxRenderer::Orientation orig_orientation = renderer.getOrientation(); renderer.setOrientation(GfxRenderer::Orientation::Portrait); const int pageHeight = renderer.getScreenHeight(); constexpr int buttonWidth = 80; constexpr int smallButtonHeight = 15; constexpr int buttonHeight = LyraMetrics::values.buttonHintsHeight; constexpr int buttonY = LyraMetrics::values.buttonHintsHeight; // Distance from bottom constexpr int textYOffset = 7; // Distance from top of button to text baseline constexpr int buttonPositions[] = {58, 146, 254, 342}; const char* labels[] = {btn1, btn2, btn3, btn4}; for (int i = 0; i < 4; i++) { const int x = buttonPositions[i]; if (labels[i] != nullptr && labels[i][0] != '\0') { // Draw the filled background and border for a FULL-sized button renderer.fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false); renderer.drawRoundedRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, 1, cornerRadius, true, true, false, false, true); const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]); const int textX = x + (buttonWidth - 1 - textWidth) / 2; renderer.drawText(SMALL_FONT_ID, textX, pageHeight - buttonY + textYOffset, labels[i]); } else { // Draw the filled background and border for a SMALL-sized button renderer.fillRect(x, pageHeight - smallButtonHeight, buttonWidth, smallButtonHeight, false); renderer.drawRoundedRect(x, pageHeight - smallButtonHeight, buttonWidth, smallButtonHeight, 1, cornerRadius, true, true, false, false, true); } } renderer.setOrientation(orig_orientation); } void LyraTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const { const int screenWidth = renderer.getScreenWidth(); constexpr int buttonWidth = LyraMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated) constexpr int buttonHeight = 78; // Height on screen (width when rotated) // Position for the button group - buttons share a border so they're adjacent const char* labels[] = {topBtn, bottomBtn}; // Draw the shared border for both buttons as one unit const int x = screenWidth - buttonWidth; // Draw top button outline if (topBtn != nullptr && topBtn[0] != '\0') { renderer.drawRoundedRect(x, topHintButtonY, buttonWidth, buttonHeight, 1, cornerRadius, true, false, true, false, true); } // Draw bottom button outline if (bottomBtn != nullptr && bottomBtn[0] != '\0') { renderer.drawRoundedRect(x, topHintButtonY + buttonHeight + 5, buttonWidth, buttonHeight, 1, cornerRadius, true, false, true, false, true); } // Draw text for each button for (int i = 0; i < 2; i++) { if (labels[i] != nullptr && labels[i][0] != '\0') { const int y = topHintButtonY + (i * buttonHeight + 5); // Draw rotated text centered in the button const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]); renderer.drawTextRotated90CW(SMALL_FONT_ID, x, y + (buttonHeight + textWidth) / 2, labels[i]); } } } void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector& recentBooks, const int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored, std::function storeCoverBuffer) const { const int bookCount = std::min(static_cast(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount); const int tileHeight = rect.height; const int tileY = rect.y; 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); 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::vector words; words.reserve(8); size_t pos = 0; while (pos < text.size()) { while (pos < text.size() && text[pos] == ' ') ++pos; if (pos >= text.size()) break; const size_t start = pos; while (pos < text.size() && text[pos] != ' ') ++pos; words.emplace_back(text.substr(start, pos - start)); } const int spaceWidth = renderer.getSpaceWidth(fontId); std::vector lines; std::string currentLine; for (auto& word : words) { if (static_cast(lines.size()) >= maxLines) { lines.back().append("..."); while (!lines.back().empty() && renderer.getTextWidth(fontId, lines.back().c_str()) > maxWidth) { lines.back().resize(lines.back().size() - 3); utf8RemoveLastChar(lines.back()); lines.back().append("..."); } break; } int wordWidth = renderer.getTextWidth(fontId, word.c_str()); while (wordWidth > maxWidth && !word.empty()) { utf8RemoveLastChar(word); std::string withEllipsis = word + "..."; wordWidth = renderer.getTextWidth(fontId, withEllipsis.c_str()); if (wordWidth <= maxWidth) { word = withEllipsis; break; } } int newLineWidth = renderer.getTextWidth(fontId, currentLine.c_str()); if (newLineWidth > 0) newLineWidth += spaceWidth; newLineWidth += wordWidth; if (newLineWidth > maxWidth && !currentLine.empty()) { lines.push_back(currentLine); currentLine = word; } else { if (!currentLine.empty()) currentLine.append(" "); currentLine.append(word); } } if (!currentLine.empty() && static_cast(lines.size()) < maxLines) { lines.push_back(currentLine); } 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) { FsFile file; if (storage.openFileForRead("HOME", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { float bmpW = static_cast(bitmap.getWidth()); float bmpH = static_cast(bitmap.getHeight()); float ratio = bmpW / bmpH; int naturalWidth = static_cast(coverHeight * ratio); if (naturalWidth >= slotWidth) { float slotRatio = static_cast(slotWidth) / static_cast(coverHeight); float cropX = 1.0f - (slotRatio / ratio); renderer.drawBitmap(bitmap, slotX, slotY, slotWidth, coverHeight, cropX); } else { int offsetX = (slotWidth - naturalWidth) / 2; renderer.drawBitmap(bitmap, slotX + offsetX, slotY, naturalWidth, coverHeight, 0.0f); } } file.close(); } }; 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(coverHeight * 0.65f); const int textGap = hPaddingInSelection * 2; const int textAreaX = cardX + hPaddingInSelection + coverSlotWidth + textGap; const int textAreaWidth = cardWidth - hPaddingInSelection * 2 - coverSlotWidth - textGap; if (!coverRendered) { renderer.drawRect(cardX + hPaddingInSelection, tileY + hPaddingInSelection, coverSlotWidth, coverHeight); if (!recentBooks[0].coverBmpPath.empty()) { const std::string coverBmpPath = UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, coverHeight); renderCoverBitmap(coverBmpPath, cardX + hPaddingInSelection, tileY + hPaddingInSelection, coverSlotWidth); } coverBufferStored = storeCoverBuffer(); 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) { renderer.fillRoundedRect(cardX, bottomY, cardWidth, bottomH, cornerRadius, false, false, true, true, Color::LightGray); } } // 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; for (const auto& line : titleLines) { renderer.drawText(UI_12_FONT_ID, textAreaX, textY, line.c_str(), true); 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); renderer.drawText(UI_10_FONT_ID, textAreaX, textY, author.c_str(), true); } } 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; int drawWidth = tileWidth - 2 * hPaddingInSelection; renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, drawWidth, coverHeight); if (!recentBooks[i].coverBmpPath.empty()) { const std::string coverBmpPath = UITheme::getCoverThumbPath(recentBooks[i].coverBmpPath, coverHeight); renderCoverBitmap(coverBmpPath, tileX + hPaddingInSelection, tileY + hPaddingInSelection, drawWidth); } } coverBufferStored = storeCoverBuffer(); 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); int textY = tileY + coverHeight + hPaddingInSelection + 4; for (const auto& line : titleLines) { renderer.drawText(UI_10_FONT_ID, tileX + hPaddingInSelection, textY, line.c_str(), true); 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); } } } } void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex, const std::function& buttonLabel, const std::function& 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(i / 2) * (LyraMetrics::values.menuRowHeight + LyraMetrics::values.menuSpacing), tileWidth, LyraMetrics::values.menuRowHeight}; const bool selected = selectedIndex == i; if (selected) { renderer.fillRoundedRect(tileRect.x, tileRect.y, tileRect.width, tileRect.height, cornerRadius, Color::LightGray); } std::string labelStr = buttonLabel(i); const char* label = labelStr.c_str(); const 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 renderer.drawText(UI_12_FONT_ID, textX, textY, label, true); } } Rect LyraTheme::drawPopup(const GfxRenderer& renderer, const char* message) const { constexpr int margin = 15; constexpr int y = 60; const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, message, EpdFontFamily::REGULAR); const int textHeight = renderer.getLineHeight(UI_12_FONT_ID); const int w = textWidth + margin * 2; const int h = textHeight + margin * 2; const int x = (renderer.getScreenWidth() - w) / 2; renderer.fillRect(x - 5, y - 5, w + 10, h + 10, false); renderer.drawRect(x, y, w, h, true); const int textX = x + (w - textWidth) / 2; const int textY = y + margin - 2; renderer.drawText(UI_12_FONT_ID, textX, textY, message, true, EpdFontFamily::REGULAR); renderer.displayBuffer(); return Rect{x, y, w, h}; }