#include "BaseTheme.h" #include #include #include #include #include #include "Battery.h" #include "RecentBooksStore.h" #include "components/UITheme.h" #include "fontIds.h" // Internal constants namespace { constexpr int batteryPercentSpacing = 4; constexpr int homeMenuMargin = 20; constexpr int homeMarginTop = 30; } // namespace void BaseTheme::drawBattery(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { // Left aligned battery icon and percentage // TODO refactor this so the percentage doesnt change after we position it const uint16_t percentage = battery.readPercentage(); if (showPercentage) { const auto percentageText = std::to_string(percentage) + "%"; renderer.drawText(SMALL_FONT_ID, rect.x + batteryPercentSpacing + BaseMetrics::values.batteryWidth, rect.y, percentageText.c_str()); } // 1 column on left, 2 columns on right, 5 columns of battery body const int x = rect.x; const int y = rect.y + 6; const int battWidth = BaseMetrics::values.batteryWidth; // 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); // The +1 is to round up, so that we always fill at least one pixel int filledWidth = percentage * (rect.width - 5) / 100 + 1; if (filledWidth > rect.width - 5) { filledWidth = rect.width - 5; // Ensure we don't overflow } renderer.fillRect(x + 2, y + 2, filledWidth, rect.height - 4); } void BaseTheme::drawProgressBar(const GfxRenderer& renderer, Rect rect, const size_t current, const size_t total) const { if (total == 0) { return; } // Use 64-bit arithmetic to avoid overflow for large files const int percent = static_cast((static_cast(current) * 100) / total); // Draw outline renderer.drawRect(rect.x, rect.y, rect.width, rect.height); // Draw filled portion const int fillWidth = (rect.width - 4) * percent / 100; if (fillWidth > 0) { renderer.fillRect(rect.x + 2, rect.y + 2, fillWidth, rect.height - 4); } // Draw percentage text centered below bar const std::string percentText = std::to_string(percent) + "%"; renderer.drawCenteredText(UI_10_FONT_ID, rect.y + rect.height + 15, percentText.c_str()); } void BaseTheme::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 = 106; constexpr int buttonHeight = BaseMetrics::values.buttonHintsHeight; constexpr int buttonY = BaseMetrics::values.buttonHintsHeight; // Distance from bottom constexpr int textYOffset = 7; // Distance from top of button to text baseline constexpr int buttonPositions[] = {25, 130, 245, 350}; const char* labels[] = {btn1, btn2, btn3, btn4}; for (int i = 0; i < 4; i++) { // Only draw if the label is non-empty if (labels[i] != nullptr && labels[i][0] != '\0') { const int x = buttonPositions[i]; renderer.fillRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, false); renderer.drawRect(x, pageHeight - buttonY, buttonWidth, buttonHeight); const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, labels[i]); const int textX = x + (buttonWidth - 1 - textWidth) / 2; renderer.drawText(UI_10_FONT_ID, textX, pageHeight - buttonY + textYOffset, labels[i]); } } renderer.setOrientation(orig_orientation); } void BaseTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const { const int screenWidth = renderer.getScreenWidth(); constexpr int buttonWidth = BaseMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated) constexpr int buttonHeight = 80; // Height on screen (width when rotated) constexpr int buttonX = 4; // Distance from right edge // Position for the button group - buttons share a border so they're adjacent constexpr int topButtonY = 345; // Top button position const char* labels[] = {topBtn, bottomBtn}; // Draw the shared border for both buttons as one unit const int x = screenWidth - buttonX - buttonWidth; // Draw top button outline (3 sides, bottom open) if (topBtn != nullptr && topBtn[0] != '\0') { renderer.drawLine(x, topButtonY, x + buttonWidth - 1, topButtonY); // Top renderer.drawLine(x, topButtonY, x, topButtonY + buttonHeight - 1); // Left renderer.drawLine(x + buttonWidth - 1, topButtonY, x + buttonWidth - 1, topButtonY + buttonHeight - 1); // Right } // Draw shared middle border if ((topBtn != nullptr && topBtn[0] != '\0') || (bottomBtn != nullptr && bottomBtn[0] != '\0')) { renderer.drawLine(x, topButtonY + buttonHeight, x + buttonWidth - 1, topButtonY + buttonHeight); // Shared border } // Draw bottom button outline (3 sides, top is shared) if (bottomBtn != nullptr && bottomBtn[0] != '\0') { renderer.drawLine(x, topButtonY + buttonHeight, x, topButtonY + 2 * buttonHeight - 1); // Left renderer.drawLine(x + buttonWidth - 1, topButtonY + buttonHeight, x + buttonWidth - 1, topButtonY + 2 * buttonHeight - 1); // Right renderer.drawLine(x, topButtonY + 2 * buttonHeight - 1, x + buttonWidth - 1, topButtonY + 2 * buttonHeight - 1); // Bottom } // Draw text for each button for (int i = 0; i < 2; i++) { if (labels[i] != nullptr && labels[i][0] != '\0') { const int y = topButtonY + i * buttonHeight; // Draw rotated text centered in the button const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]); const int textHeight = renderer.getTextHeight(SMALL_FONT_ID); // Center the rotated text in the button const int textX = x + (buttonWidth - textHeight) / 2; const int textY = y + (buttonHeight + textWidth) / 2; renderer.drawTextRotated90CW(SMALL_FONT_ID, textX, textY, labels[i]); } } } void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, const std::function& rowTitle, bool hasSubtitle, const std::function& rowSubtitle, bool hasIcon, const std::function& rowIcon, bool hasValue, const std::function& rowValue) const { int rowHeight = hasSubtitle ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight; int pageItems = rect.height / rowHeight; const int totalPages = (itemCount + pageItems - 1) / pageItems; if (totalPages > 1) { constexpr int indicatorWidth = 20; constexpr int arrowSize = 6; constexpr int margin = 15; // Offset from right edge const int centerX = rect.x + rect.width - indicatorWidth / 2 - margin; const int indicatorTop = rect.y; // Offset to avoid overlapping side button hints const int indicatorBottom = rect.y + rect.height - 30; // Draw up arrow at top (^) - narrow point at top, wide base at bottom for (int i = 0; i < arrowSize; ++i) { const int lineWidth = 1 + i * 2; const int startX = centerX - i; renderer.drawLine(startX, indicatorTop + i, startX + lineWidth - 1, indicatorTop + i); } // Draw down arrow at bottom (v) - wide base at top, narrow point at bottom for (int i = 0; i < arrowSize; ++i) { const int lineWidth = 1 + (arrowSize - 1 - i) * 2; const int startX = centerX - (arrowSize - 1 - i); renderer.drawLine(startX, indicatorBottom - arrowSize + 1 + i, startX + lineWidth - 1, indicatorBottom - arrowSize + 1 + i); } } // Draw selection int contentWidth = rect.width - 5; if (selectedIndex >= 0) { renderer.fillRect(0, rect.y + selectedIndex % pageItems * rowHeight - 2, rect.width, rowHeight); } // 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; int textWidth = contentWidth - BaseMetrics::values.contentSidePadding * 2 - (hasValue ? 60 : 0); // Draw name auto itemName = rowTitle(i); auto font = hasSubtitle ? UI_12_FONT_ID : UI_10_FONT_ID; auto item = renderer.truncatedText(font, itemName.c_str(), textWidth); renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding, itemY, item.c_str(), i != selectedIndex); if (hasSubtitle) { // Draw subtitle std::string subtitleText = rowSubtitle(i); auto subtitle = renderer.truncatedText(UI_10_FONT_ID, subtitleText.c_str(), textWidth); renderer.drawText(UI_10_FONT_ID, rect.x + BaseMetrics::values.contentSidePadding, itemY + 30, subtitle.c_str(), i != selectedIndex); } if (hasValue) { // Draw value std::string valueText = rowValue(i); const auto valueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()); renderer.drawText(UI_10_FONT_ID, rect.x + contentWidth - BaseMetrics::values.contentSidePadding - valueTextWidth, itemY, valueText.c_str(), i != selectedIndex); } } } void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const { const bool showBatteryPercentage = SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS; int batteryX = rect.x + rect.width - BaseMetrics::values.contentSidePadding - BaseMetrics::values.batteryWidth; if (showBatteryPercentage) { const uint16_t percentage = battery.readPercentage(); const auto percentageText = std::to_string(percentage) + "%"; batteryX -= renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str()); } drawBattery(renderer, Rect{batteryX, rect.y + 5, BaseMetrics::values.batteryWidth, BaseMetrics::values.batteryHeight}, showBatteryPercentage); if (title) { int padding = rect.width - batteryX; auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title, rect.width - padding * 2 - BaseMetrics::values.contentSidePadding * 2, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, rect.y + 5, truncatedTitle.c_str(), true, EpdFontFamily::BOLD); } } void BaseTheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const std::vector& tabs, bool selected) const { constexpr int underlineHeight = 2; // Height of selection underline constexpr int underlineGap = 4; // Gap between text and underline const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); int currentX = rect.x + BaseMetrics::values.contentSidePadding; for (const auto& tab : tabs) { const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); // Draw underline for selected tab if (tab.selected) { if (selected) { renderer.fillRect(currentX - 3, rect.y, textWidth + 6, lineHeight + underlineGap); } else { renderer.fillRect(currentX, rect.y + lineHeight + underlineGap, textWidth, underlineHeight); } } // Draw tab label renderer.drawText(UI_12_FONT_ID, currentX, rect.y, tab.label, !(tab.selected && selected), tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); currentX += textWidth + BaseMetrics::values.tabSpacing; } } // Draw the "Recent Book" cover card on the home screen // TODO: Refactor method to make it cleaner, split into smaller methods void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector& recentBooks, const int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored, std::function storeCoverBuffer) const { // --- Top "book" card for the current title (selectorIndex == 0) --- const int bookWidth = rect.width / 2; const int bookHeight = rect.height; const int bookX = (rect.width - bookWidth) / 2; const int bookY = rect.y; const bool hasContinueReading = !recentBooks.empty(); const bool bookSelected = hasContinueReading && selectorIndex == 0; // Bookmark dimensions (used in multiple places) const int bookmarkWidth = bookWidth / 8; const int bookmarkHeight = bookHeight / 5; const int bookmarkX = bookX + bookWidth - bookmarkWidth - 10; const int bookmarkY = bookY + 5; // Draw book card regardless, fill with message based on `hasContinueReading` { // Draw cover image as background if available (inside the box) // Only load from SD on first render, then use stored buffer if (hasContinueReading && !recentBooks[0].coverBmpPath.empty() && !coverRendered) { const std::string coverBmpPath = UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, BaseMetrics::values.homeCoverHeight); // First time: load cover from SD and render FsFile file; if (SdMan.openFileForRead("HOME", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { // Calculate position to center image within the book card int coverX, coverY; if (bitmap.getWidth() > bookWidth || bitmap.getHeight() > bookHeight) { const float imgRatio = static_cast(bitmap.getWidth()) / static_cast(bitmap.getHeight()); const float boxRatio = static_cast(bookWidth) / static_cast(bookHeight); if (imgRatio > boxRatio) { coverX = bookX; coverY = bookY + (bookHeight - static_cast(bookWidth / imgRatio)) / 2; } else { coverX = bookX + (bookWidth - static_cast(bookHeight * imgRatio)) / 2; coverY = bookY; } } else { coverX = bookX + (bookWidth - bitmap.getWidth()) / 2; coverY = bookY + (bookHeight - bitmap.getHeight()) / 2; } // Draw the cover image centered within the book card renderer.drawBitmap(bitmap, coverX, coverY, bookWidth, bookHeight); // Draw border around the card renderer.drawRect(bookX, bookY, bookWidth, bookHeight); // No bookmark ribbon when cover is shown - it would just cover the art // Store the buffer with cover image for fast navigation coverBufferStored = storeCoverBuffer(); coverRendered = true; // First render: if selected, draw selection indicators now if (bookSelected) { renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2); renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4); } } file.close(); } } else if (!bufferRestored && !coverRendered) { // No cover image: draw border or fill, plus bookmark as visual flair if (bookSelected) { renderer.fillRect(bookX, bookY, bookWidth, bookHeight); } else { renderer.drawRect(bookX, bookY, bookWidth, bookHeight); } // Draw bookmark ribbon when no cover image (visual decoration) if (hasContinueReading) { const int notchDepth = bookmarkHeight / 3; const int centerX = bookmarkX + bookmarkWidth / 2; const int xPoints[5] = { bookmarkX, // top-left bookmarkX + bookmarkWidth, // top-right bookmarkX + bookmarkWidth, // bottom-right centerX, // center notch point bookmarkX // bottom-left }; const int yPoints[5] = { bookmarkY, // top-left bookmarkY, // top-right bookmarkY + bookmarkHeight, // bottom-right bookmarkY + bookmarkHeight - notchDepth, // center notch point bookmarkY + bookmarkHeight // bottom-left }; // Draw bookmark ribbon (inverted if selected) renderer.fillPolygon(xPoints, yPoints, 5, !bookSelected); } } // If buffer was restored, draw selection indicators if needed if (bufferRestored && bookSelected && coverRendered) { // Draw selection border (no bookmark inversion needed since cover has no bookmark) renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2); renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4); } else if (!coverRendered && !bufferRestored) { // Selection border already handled above in the no-cover case } } if (hasContinueReading) { const std::string& lastBookTitle = recentBooks[0].title; const std::string& lastBookAuthor = recentBooks[0].author; // Invert text colors based on selection state: // - With cover: selected = white text on black box, unselected = black text on white box // - Without cover: selected = white text on black card, unselected = black text on white card // Split into words (avoid stringstream to keep this light on the MCU) std::vector words; words.reserve(8); size_t pos = 0; while (pos < lastBookTitle.size()) { while (pos < lastBookTitle.size() && lastBookTitle[pos] == ' ') { ++pos; } if (pos >= lastBookTitle.size()) { break; } const size_t start = pos; while (pos < lastBookTitle.size() && lastBookTitle[pos] != ' ') { ++pos; } words.emplace_back(lastBookTitle.substr(start, pos - start)); } std::vector lines; std::string currentLine; // Extra padding inside the card so text doesn't hug the border const int maxLineWidth = bookWidth - 40; const int spaceWidth = renderer.getSpaceWidth(UI_12_FONT_ID); for (auto& i : words) { // If we just hit the line limit (3), stop processing words if (lines.size() >= 3) { // Limit to 3 lines // Still have words left, so add ellipsis to last line lines.back().append("..."); while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) { // Remove "..." first, then remove one UTF-8 char, then add "..." back lines.back().resize(lines.back().size() - 3); // Remove "..." utf8RemoveLastChar(lines.back()); lines.back().append("..."); } break; } int wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str()); while (wordWidth > maxLineWidth && !i.empty()) { // Word itself is too long, trim it (UTF-8 safe) utf8RemoveLastChar(i); // Check if we have room for ellipsis std::string withEllipsis = i + "..."; wordWidth = renderer.getTextWidth(UI_12_FONT_ID, withEllipsis.c_str()); if (wordWidth <= maxLineWidth) { i = withEllipsis; break; } } int newLineWidth = renderer.getTextWidth(UI_12_FONT_ID, currentLine.c_str()); if (newLineWidth > 0) { newLineWidth += spaceWidth; } newLineWidth += wordWidth; if (newLineWidth > maxLineWidth && !currentLine.empty()) { // New line too long, push old line lines.push_back(currentLine); currentLine = i; } else { currentLine.append(" ").append(i); } } // If lower than the line limit, push remaining words if (!currentLine.empty() && lines.size() < 3) { lines.push_back(currentLine); } // Book title text int totalTextHeight = renderer.getLineHeight(UI_12_FONT_ID) * static_cast(lines.size()); if (!lastBookAuthor.empty()) { totalTextHeight += renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2; } // Vertically center the title block within the card int titleYStart = bookY + (bookHeight - totalTextHeight) / 2; // If cover image was rendered, draw box behind title and author if (coverRendered) { constexpr int boxPadding = 8; // Calculate the max text width for the box int maxTextWidth = 0; for (const auto& line : lines) { const int lineWidth = renderer.getTextWidth(UI_12_FONT_ID, line.c_str()); if (lineWidth > maxTextWidth) { maxTextWidth = lineWidth; } } if (!lastBookAuthor.empty()) { std::string trimmedAuthor = lastBookAuthor; while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) { utf8RemoveLastChar(trimmedAuthor); } if (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) < renderer.getTextWidth(UI_10_FONT_ID, lastBookAuthor.c_str())) { trimmedAuthor.append("..."); } const int authorWidth = renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()); if (authorWidth > maxTextWidth) { maxTextWidth = authorWidth; } } const int boxWidth = maxTextWidth + boxPadding * 2; const int boxHeight = totalTextHeight + boxPadding * 2; const int boxX = (rect.width - boxWidth) / 2; const int boxY = titleYStart - boxPadding; // Draw box (inverted when selected: black box instead of white) renderer.fillRect(boxX, boxY, boxWidth, boxHeight, bookSelected); // Draw border around the box (inverted when selected: white border instead of black) renderer.drawRect(boxX, boxY, boxWidth, boxHeight, !bookSelected); } for (const auto& line : lines) { renderer.drawCenteredText(UI_12_FONT_ID, titleYStart, line.c_str(), !bookSelected); titleYStart += renderer.getLineHeight(UI_12_FONT_ID); } if (!lastBookAuthor.empty()) { titleYStart += renderer.getLineHeight(UI_10_FONT_ID) / 2; std::string trimmedAuthor = lastBookAuthor; // Trim author if too long (UTF-8 safe) bool wasTrimmed = false; while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) { utf8RemoveLastChar(trimmedAuthor); wasTrimmed = true; } if (wasTrimmed && !trimmedAuthor.empty()) { // Make room for ellipsis while (renderer.getTextWidth(UI_10_FONT_ID, (trimmedAuthor + "...").c_str()) > maxLineWidth && !trimmedAuthor.empty()) { utf8RemoveLastChar(trimmedAuthor); } trimmedAuthor.append("..."); } renderer.drawCenteredText(UI_10_FONT_ID, titleYStart, trimmedAuthor.c_str(), !bookSelected); } // "Continue Reading" label at the bottom const int continueY = bookY + bookHeight - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2; if (coverRendered) { // Draw box behind "Continue Reading" text (inverted when selected: black box instead of white) const char* continueText = "Continue Reading"; const int continueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, continueText); constexpr int continuePadding = 6; const int continueBoxWidth = continueTextWidth + continuePadding * 2; const int continueBoxHeight = renderer.getLineHeight(UI_10_FONT_ID) + continuePadding; const int continueBoxX = (rect.width - continueBoxWidth) / 2; const int continueBoxY = continueY - continuePadding / 2; renderer.fillRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, bookSelected); renderer.drawRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, !bookSelected); renderer.drawCenteredText(UI_10_FONT_ID, continueY, continueText, !bookSelected); } else { renderer.drawCenteredText(UI_10_FONT_ID, continueY, "Continue Reading", !bookSelected); } } else { // No book to continue reading const int y = bookY + (bookHeight - renderer.getLineHeight(UI_12_FONT_ID) - renderer.getLineHeight(UI_10_FONT_ID)) / 2; renderer.drawCenteredText(UI_12_FONT_ID, y, "No open book"); renderer.drawCenteredText(UI_10_FONT_ID, y + renderer.getLineHeight(UI_12_FONT_ID), "Start reading below"); } } void BaseTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex, const std::function& buttonLabel, bool hasIcon, const std::function& rowIcon) const { for (int i = 0; i < buttonCount; ++i) { const int tileY = BaseMetrics::values.verticalSpacing + rect.y + static_cast(i) * (BaseMetrics::values.menuRowHeight + BaseMetrics::values.menuSpacing); const bool selected = selectedIndex == i; if (selected) { renderer.fillRect(rect.x + BaseMetrics::values.contentSidePadding, tileY, rect.width - BaseMetrics::values.contentSidePadding * 2, BaseMetrics::values.menuRowHeight); } else { renderer.drawRect(rect.x + BaseMetrics::values.contentSidePadding, tileY, rect.width - BaseMetrics::values.contentSidePadding * 2, BaseMetrics::values.menuRowHeight); } const char* label = buttonLabel(i).c_str(); const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, label); const int textX = rect.x + (rect.width - textWidth) / 2; const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID); const int textY = tileY + (BaseMetrics::values.menuRowHeight - lineHeight) / 2; // vertically centered assuming y is top of text // Invert text when the tile is selected, to contrast with the filled background renderer.drawText(UI_10_FONT_ID, textX, textY, label, selectedIndex != i); } } Rect BaseTheme::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::BOLD); 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 - 2, y - 2, w + 4, h + 4, true); // frame thickness 2 renderer.fillRect(x, y, w, h, false); const int textX = x + (w - textWidth) / 2; const int textY = y + margin - 2; renderer.drawText(UI_12_FONT_ID, textX, textY, message, true, EpdFontFamily::BOLD); renderer.displayBuffer(); return Rect{x, y, w, h}; } void BaseTheme::fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const { constexpr int barHeight = 4; const int barWidth = layout.width - 30; // twice the margin in drawPopup to match text width const int barX = layout.x + (layout.width - barWidth) / 2; const int barY = layout.y + layout.height - 10; int fillWidth = barWidth * progress / 100; renderer.fillRect(barX, barY, fillWidth, barHeight, true); renderer.displayBuffer(HalDisplay::FAST_REFRESH); } void BaseTheme::drawBookProgressBar(const GfxRenderer& renderer, const size_t bookProgress) const { int vieweableMarginTop, vieweableMarginRight, vieweableMarginBottom, vieweableMarginLeft; renderer.getOrientedViewableTRBL(&vieweableMarginTop, &vieweableMarginRight, &vieweableMarginBottom, &vieweableMarginLeft); const int progressBarMaxWidth = renderer.getScreenWidth() - vieweableMarginLeft - vieweableMarginRight; const int progressBarY = renderer.getScreenHeight() - vieweableMarginBottom - BaseMetrics::values.bookProgressBarHeight; const int barWidth = progressBarMaxWidth * bookProgress / 100; renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BaseMetrics::values.bookProgressBarHeight, true); }