883 lines
40 KiB
C++
883 lines
40 KiB
C++
#include "BaseTheme.h"
|
|
|
|
#include <GfxRenderer.h>
|
|
#include <HalClock.h>
|
|
#include <HalPowerManager.h>
|
|
#include <HalStorage.h>
|
|
#include <Logging.h>
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <string>
|
|
|
|
#include "I18n.h"
|
|
#include "RecentBooksStore.h"
|
|
#include "components/UITheme.h"
|
|
#include "fontIds.h"
|
|
|
|
// Internal constants
|
|
namespace {
|
|
constexpr int homeMenuMargin = 20;
|
|
constexpr int homeMarginTop = 30;
|
|
constexpr int subtitleY = 738;
|
|
|
|
// Helper: draw battery icon at given position
|
|
void drawBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, uint16_t percentage) {
|
|
// Draw battery outline (shared code)
|
|
BaseTheme::drawBatteryOutline(renderer, x, y, battWidth, rectHeight);
|
|
|
|
const bool charging = gpio.isUsbConnected();
|
|
|
|
// The +1 is to round up, so that we always fill at least one pixel
|
|
const int maxFillWidth = battWidth - 5;
|
|
const int fillHeight = rectHeight - 4;
|
|
if (maxFillWidth <= 0 || fillHeight <= 0) {
|
|
return;
|
|
}
|
|
int filledWidth = percentage * maxFillWidth / 100 + 1;
|
|
if (filledWidth > maxFillWidth) {
|
|
filledWidth = maxFillWidth;
|
|
}
|
|
|
|
// When charging, ensure minimum fill so lightning bolt is fully visible
|
|
constexpr int minFillForBolt = 8;
|
|
if (charging && filledWidth < minFillForBolt) {
|
|
filledWidth = std::min(minFillForBolt, maxFillWidth);
|
|
}
|
|
|
|
renderer.fillRect(x + 2, y + 2, filledWidth, fillHeight);
|
|
|
|
// Draw lightning bolt when charging (white/inverted on black fill for visibility)
|
|
if (charging) {
|
|
BaseTheme::drawBatteryLightningBolt(renderer, x + 4, y + 2);
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
void BaseTheme::drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight) {
|
|
// Top line
|
|
renderer.drawLine(x + 1, y, x + battWidth - 3, y);
|
|
// Bottom line
|
|
renderer.drawLine(x + 1, y + rectHeight - 1, x + battWidth - 3, y + rectHeight - 1);
|
|
// Left line
|
|
renderer.drawLine(x, y + 1, x, y + rectHeight - 2);
|
|
// Battery end
|
|
renderer.drawLine(x + battWidth - 2, y + 1, x + battWidth - 2, y + rectHeight - 2);
|
|
renderer.drawPixel(x + battWidth - 1, y + 3);
|
|
renderer.drawPixel(x + battWidth - 1, y + rectHeight - 4);
|
|
renderer.drawLine(x + battWidth - 0, y + 4, x + battWidth - 0, y + rectHeight - 5);
|
|
}
|
|
|
|
void BaseTheme::drawBatteryLightningBolt(const GfxRenderer& renderer, int boltX, int boltY) {
|
|
// Draw lightning bolt (white/inverted on black fill for visibility)
|
|
renderer.drawLine(boltX + 4, boltY + 0, boltX + 5, boltY + 0, false);
|
|
renderer.drawLine(boltX + 3, boltY + 1, boltX + 4, boltY + 1, false);
|
|
renderer.drawLine(boltX + 2, boltY + 2, boltX + 5, boltY + 2, false);
|
|
renderer.drawLine(boltX + 3, boltY + 3, boltX + 4, boltY + 3, false);
|
|
renderer.drawLine(boltX + 2, boltY + 4, boltX + 3, boltY + 4, false);
|
|
renderer.drawLine(boltX + 1, boltY + 5, boltX + 4, boltY + 5, false);
|
|
renderer.drawLine(boltX + 2, boltY + 6, boltX + 3, boltY + 6, false);
|
|
renderer.drawLine(boltX + 1, boltY + 7, boltX + 2, boltY + 7, false);
|
|
}
|
|
|
|
void BaseTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
|
|
// Left aligned: icon on left, percentage on right (reader mode)
|
|
const uint16_t percentage = powerManager.getBatteryPercentage();
|
|
const int y = rect.y + 6;
|
|
|
|
if (showPercentage) {
|
|
const auto percentageText = std::to_string(percentage) + "%";
|
|
renderer.drawText(SMALL_FONT_ID, rect.x + BaseTheme::batteryPercentSpacing + BaseMetrics::values.batteryWidth,
|
|
rect.y, percentageText.c_str());
|
|
}
|
|
|
|
drawBatteryIcon(renderer, rect.x, y, BaseMetrics::values.batteryWidth, rect.height, percentage);
|
|
}
|
|
|
|
void BaseTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
|
|
// Right aligned: percentage on left, icon on right (UI headers)
|
|
// rect.x is already positioned for the icon (drawHeader calculated it)
|
|
const uint16_t percentage = powerManager.getBatteryPercentage();
|
|
const int y = rect.y + 6;
|
|
|
|
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 - BaseTheme::batteryPercentSpacing, rect.y, textWidth, textHeight, false);
|
|
// Draw text to the left of the icon
|
|
renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - BaseTheme::batteryPercentSpacing, rect.y,
|
|
percentageText.c_str());
|
|
}
|
|
|
|
// Icon is already at correct position from rect.x
|
|
drawBatteryIcon(renderer, rect.x, y, BaseMetrics::values.batteryWidth, rect.height, percentage);
|
|
}
|
|
|
|
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<int>((static_cast<uint64_t>(current) * 100) / total);
|
|
|
|
LOG_DBG("UI", "Drawing progress bar: current=%u, total=%u, percent=%d", current, total, percent);
|
|
// 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
|
|
// X3 has wider screen in portrait (528 vs 480), use more spacing
|
|
constexpr int x4ButtonPositions[] = {25, 130, 245, 350};
|
|
constexpr int x3ButtonPositions[] = {38, 154, 268, 384};
|
|
const int* buttonPositions = gpio.deviceIsX3() ? x3ButtonPositions : x4ButtonPositions;
|
|
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 buttonMargin = 4;
|
|
|
|
if (gpio.deviceIsX3()) {
|
|
// X3 layout: Up on left side, Down on right side, positioned higher
|
|
constexpr int x3ButtonY = 155;
|
|
|
|
if (topBtn != nullptr && topBtn[0] != '\0') {
|
|
const int leftX = buttonMargin;
|
|
renderer.drawRect(leftX, x3ButtonY, buttonWidth, buttonHeight);
|
|
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, topBtn);
|
|
const int textHeight = renderer.getTextHeight(SMALL_FONT_ID);
|
|
const int textX = leftX + (buttonWidth - textHeight) / 2;
|
|
const int textY = x3ButtonY + (buttonHeight + textWidth) / 2;
|
|
renderer.drawTextRotated90CW(SMALL_FONT_ID, textX, textY, topBtn);
|
|
}
|
|
|
|
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
|
const int rightX = screenWidth - buttonMargin - buttonWidth;
|
|
renderer.drawRect(rightX, x3ButtonY, buttonWidth, buttonHeight);
|
|
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, bottomBtn);
|
|
const int textHeight = renderer.getTextHeight(SMALL_FONT_ID);
|
|
const int textX = rightX + (buttonWidth - textHeight) / 2;
|
|
const int textY = x3ButtonY + (buttonHeight + textWidth) / 2;
|
|
renderer.drawTextRotated90CW(SMALL_FONT_ID, textX, textY, bottomBtn);
|
|
}
|
|
} else {
|
|
// X4 layout: Both buttons stacked on right side
|
|
constexpr int topButtonY = 345;
|
|
const char* labels[] = {topBtn, bottomBtn};
|
|
const int x = screenWidth - buttonMargin - buttonWidth;
|
|
|
|
if (topBtn != nullptr && topBtn[0] != '\0') {
|
|
renderer.drawLine(x, topButtonY, x + buttonWidth - 1, topButtonY);
|
|
renderer.drawLine(x, topButtonY, x, topButtonY + buttonHeight - 1);
|
|
renderer.drawLine(x + buttonWidth - 1, topButtonY, x + buttonWidth - 1, topButtonY + buttonHeight - 1);
|
|
}
|
|
|
|
if ((topBtn != nullptr && topBtn[0] != '\0') || (bottomBtn != nullptr && bottomBtn[0] != '\0')) {
|
|
renderer.drawLine(x, topButtonY + buttonHeight, x + buttonWidth - 1, topButtonY + buttonHeight);
|
|
}
|
|
|
|
if (bottomBtn != nullptr && bottomBtn[0] != '\0') {
|
|
renderer.drawLine(x, topButtonY + buttonHeight, x, topButtonY + 2 * buttonHeight - 1);
|
|
renderer.drawLine(x + buttonWidth - 1, topButtonY + buttonHeight, x + buttonWidth - 1,
|
|
topButtonY + 2 * buttonHeight - 1);
|
|
renderer.drawLine(x, topButtonY + 2 * buttonHeight - 1, x + buttonWidth - 1, topButtonY + 2 * buttonHeight - 1);
|
|
}
|
|
|
|
for (int i = 0; i < 2; i++) {
|
|
if (labels[i] != nullptr && labels[i][0] != '\0') {
|
|
const int y = topButtonY + i * buttonHeight;
|
|
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, labels[i]);
|
|
const int textHeight = renderer.getTextHeight(SMALL_FONT_ID);
|
|
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<std::string(int index)>& rowTitle,
|
|
const std::function<std::string(int index)>& rowSubtitle,
|
|
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;
|
|
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 - arrowSize;
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
bool selectedIsSeparator = false;
|
|
if (selectedIndex >= 0 && selectedIndex < itemCount) {
|
|
selectedIsSeparator = UITheme::isSeparatorTitle(rowTitle(selectedIndex));
|
|
}
|
|
|
|
// Draw selection
|
|
int contentWidth = rect.width - 5;
|
|
if (selectedIndex >= 0 && !selectedIsSeparator) {
|
|
renderer.fillRect(rect.x, 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 - (rowValue != nullptr ? 60 : 0);
|
|
|
|
// Draw name
|
|
auto itemName = rowTitle(i);
|
|
const bool isSeparator = UITheme::isSeparatorTitle(itemName);
|
|
if (isSeparator) {
|
|
itemName = UITheme::stripSeparatorTitle(itemName);
|
|
}
|
|
|
|
if (isSeparator) {
|
|
drawListSeparator(renderer,
|
|
Rect{rect.x + BaseMetrics::values.contentSidePadding, itemY,
|
|
contentWidth - BaseMetrics::values.contentSidePadding * 2, rowHeight},
|
|
rect.x + BaseMetrics::values.contentSidePadding, textWidth, itemName);
|
|
continue;
|
|
}
|
|
|
|
auto font = (rowSubtitle != nullptr) ? 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(), true);
|
|
renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding, itemY, item.c_str(), true);
|
|
|
|
if (rowSubtitle != nullptr) {
|
|
// Draw subtitle; if the text is newline-separated (author\nseries), join with • for single-line display
|
|
std::string subtitleText = rowSubtitle(i);
|
|
const auto nl = subtitleText.find('\n');
|
|
if (nl != std::string::npos) {
|
|
subtitleText.replace(nl, 1, " \u2022 ");
|
|
}
|
|
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 (rowValue != nullptr) {
|
|
// 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::drawListSeparator(const GfxRenderer& renderer, Rect rowRect, int textX, int textWidth,
|
|
const std::string& title) const {
|
|
const std::string item = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), textWidth);
|
|
const int lineY = rowRect.y + rowRect.height - 2;
|
|
renderer.drawLine(rowRect.x, lineY, rowRect.x + rowRect.width - 1, lineY, true);
|
|
renderer.drawText(SMALL_FONT_ID, textX, rowRect.y + 7, item.c_str(), true, EpdFontFamily::BOLD);
|
|
}
|
|
|
|
void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const {
|
|
// Hide last battery draw
|
|
constexpr int maxBatteryWidth = 80;
|
|
renderer.fillRect(rect.x + rect.width - maxBatteryWidth, rect.y + 5, maxBatteryWidth,
|
|
BaseMetrics::values.batteryHeight + 10, 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 - BaseMetrics::values.batteryWidth;
|
|
drawBatteryRight(renderer,
|
|
Rect{batteryX, rect.y + 5, BaseMetrics::values.batteryWidth, BaseMetrics::values.batteryHeight},
|
|
showBatteryPercentage);
|
|
|
|
// Draw clock in header
|
|
if (SETTINGS.useClock) {
|
|
char clockStr[16];
|
|
HalClock::formatTime(clockStr, sizeof(clockStr), !SETTINGS.clockFormat12h);
|
|
renderer.drawText(SMALL_FONT_ID, rect.x + BaseMetrics::values.contentSidePadding, rect.y + 5, clockStr);
|
|
}
|
|
|
|
if (title) {
|
|
int padding = rect.width - batteryX + BaseMetrics::values.batteryWidth;
|
|
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);
|
|
}
|
|
|
|
if (subtitle) {
|
|
auto truncatedSubtitle = renderer.truncatedText(
|
|
SMALL_FONT_ID, subtitle, rect.width - BaseMetrics::values.contentSidePadding * 2, EpdFontFamily::REGULAR);
|
|
int truncatedSubtitleWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedSubtitle.c_str());
|
|
renderer.drawText(SMALL_FONT_ID,
|
|
rect.x + rect.width - BaseMetrics::values.contentSidePadding - truncatedSubtitleWidth, subtitleY,
|
|
truncatedSubtitle.c_str(), true);
|
|
}
|
|
}
|
|
|
|
void BaseTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, const char* rightLabel) const {
|
|
constexpr int underlineHeight = 2; // Height of selection underline
|
|
constexpr int underlineGap = 4; // Gap between text and underline
|
|
constexpr int maxListValueWidth = 200;
|
|
|
|
int currentX = rect.x + BaseMetrics::values.contentSidePadding;
|
|
int rightSpace = BaseMetrics::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 - BaseMetrics::values.contentSidePadding - rightLabelWidth,
|
|
rect.y + 7, truncatedRightLabel.c_str());
|
|
rightSpace += rightLabelWidth + 10;
|
|
}
|
|
|
|
auto truncatedLabel = renderer.truncatedText(
|
|
UI_12_FONT_ID, label, rect.width - BaseMetrics::values.contentSidePadding - rightSpace, EpdFontFamily::REGULAR);
|
|
renderer.drawText(UI_12_FONT_ID, currentX, rect.y, truncatedLabel.c_str(), true, EpdFontFamily::REGULAR);
|
|
}
|
|
|
|
void BaseTheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const std::vector<TabInfo>& 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<RecentBook>& recentBooks,
|
|
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
|
bool& bufferRestored, std::function<bool()> storeCoverBuffer) const {
|
|
const bool hasContinueReading = !recentBooks.empty();
|
|
const bool bookSelected = hasContinueReading && selectorIndex == 0;
|
|
|
|
// --- Top "book" card for the current title (selectorIndex == 0) ---
|
|
// When there's no cover image, use fixed size (half screen)
|
|
// When there's cover image, adapt width to image aspect ratio, keep height fixed at 400px
|
|
const int baseHeight = rect.height; // Fixed height (400px)
|
|
|
|
int bookWidth, bookX;
|
|
bool hasCoverImage = false;
|
|
|
|
if (hasContinueReading && !recentBooks[0].coverBmpPath.empty()) {
|
|
// Try to get actual image dimensions from BMP header
|
|
const std::string coverBmpPath =
|
|
UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, BaseMetrics::values.homeCoverHeight);
|
|
|
|
FsFile file;
|
|
if (Storage.openFileForRead("HOME", coverBmpPath, file)) {
|
|
Bitmap bitmap(file);
|
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
|
hasCoverImage = true;
|
|
const int imgWidth = bitmap.getWidth();
|
|
const int imgHeight = bitmap.getHeight();
|
|
|
|
// Calculate width based on aspect ratio, maintaining baseHeight
|
|
if (imgWidth > 0 && imgHeight > 0) {
|
|
const float aspectRatio = static_cast<float>(imgWidth) / static_cast<float>(imgHeight);
|
|
bookWidth = static_cast<int>(baseHeight * aspectRatio);
|
|
|
|
// Ensure width doesn't exceed reasonable limits (max 90% of screen width)
|
|
const int maxWidth = static_cast<int>(rect.width * 0.9f);
|
|
if (bookWidth > maxWidth) {
|
|
bookWidth = maxWidth;
|
|
}
|
|
} else {
|
|
bookWidth = rect.width / 2; // Fallback
|
|
}
|
|
}
|
|
file.close();
|
|
}
|
|
}
|
|
|
|
if (!hasCoverImage) {
|
|
// No cover: use half screen size
|
|
bookWidth = rect.width / 2;
|
|
}
|
|
|
|
bookX = rect.x + (rect.width - bookWidth) / 2;
|
|
const int bookY = rect.y;
|
|
const int bookHeight = baseHeight;
|
|
|
|
// 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 (Storage.openFileForRead("HOME", coverBmpPath, file)) {
|
|
Bitmap bitmap(file);
|
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
|
LOG_DBG("THEME", "Rendering bmp");
|
|
|
|
// Draw the cover image (bookWidth and bookHeight already match image aspect ratio)
|
|
renderer.drawBitmap(bitmap, bookX, bookY, 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 = coverBufferStored; // Only consider it rendered if we successfully stored the buffer
|
|
|
|
// First render: if selected, draw selection indicators now
|
|
if (bookSelected) {
|
|
LOG_DBG("THEME", "Drawing selection");
|
|
renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2);
|
|
renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4);
|
|
}
|
|
}
|
|
file.close();
|
|
}
|
|
}
|
|
|
|
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;
|
|
const std::string& lastBookSeries = recentBooks[0].series;
|
|
|
|
// 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
|
|
|
|
auto lines = renderer.wrappedText(UI_12_FONT_ID, lastBookTitle.c_str(), bookWidth - 40, 3);
|
|
|
|
// Book title text
|
|
int totalTextHeight = renderer.getLineHeight(UI_12_FONT_ID) * static_cast<int>(lines.size());
|
|
if (!lastBookAuthor.empty()) {
|
|
totalTextHeight += renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2;
|
|
}
|
|
if (!lastBookSeries.empty()) {
|
|
totalTextHeight += renderer.getLineHeight(UI_10_FONT_ID);
|
|
}
|
|
|
|
// Vertically center the title block within the card
|
|
int titleYStart = bookY + (bookHeight - totalTextHeight) / 2;
|
|
|
|
const auto truncatedAuthor = lastBookAuthor.empty()
|
|
? std::string{}
|
|
: renderer.truncatedText(UI_10_FONT_ID, lastBookAuthor.c_str(), bookWidth - 40);
|
|
const auto truncatedSeries = lastBookSeries.empty()
|
|
? std::string{}
|
|
: renderer.truncatedText(UI_10_FONT_ID, lastBookSeries.c_str(), bookWidth - 40);
|
|
|
|
// If cover image was rendered, draw box behind title, author, and series
|
|
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 (!truncatedAuthor.empty()) {
|
|
const int authorWidth = renderer.getTextWidth(UI_10_FONT_ID, truncatedAuthor.c_str());
|
|
if (authorWidth > maxTextWidth) {
|
|
maxTextWidth = authorWidth;
|
|
}
|
|
}
|
|
if (!truncatedSeries.empty()) {
|
|
const int seriesWidth = renderer.getTextWidth(UI_10_FONT_ID, truncatedSeries.c_str());
|
|
if (seriesWidth > maxTextWidth) {
|
|
maxTextWidth = seriesWidth;
|
|
}
|
|
}
|
|
|
|
const int boxWidth = maxTextWidth + boxPadding * 2;
|
|
const int boxHeight = totalTextHeight + boxPadding * 2;
|
|
const int boxX = rect.x + (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 (!truncatedAuthor.empty()) {
|
|
titleYStart += renderer.getLineHeight(UI_10_FONT_ID) / 2;
|
|
renderer.drawCenteredText(UI_10_FONT_ID, titleYStart, truncatedAuthor.c_str(), !bookSelected);
|
|
titleYStart += renderer.getLineHeight(UI_10_FONT_ID);
|
|
}
|
|
|
|
if (!truncatedSeries.empty()) {
|
|
renderer.drawCenteredText(UI_10_FONT_ID, titleYStart, truncatedSeries.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 = tr(STR_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.x + (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, tr(STR_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<std::string(int index)>& buttonLabel,
|
|
const std::function<UIIcon(int index)>& rowIcon) const {
|
|
int rowHeight = BaseMetrics::values.menuRowHeight;
|
|
int rowSpacing = BaseMetrics::values.menuSpacing;
|
|
if (buttonCount > 0 && rect.height > 0) {
|
|
const int defaultHeight = buttonCount * rowHeight + std::max(0, buttonCount - 1) * rowSpacing;
|
|
if (defaultHeight > rect.height) {
|
|
rowSpacing = std::max(0, (rect.height - buttonCount * rowHeight) / std::max(1, buttonCount - 1));
|
|
if (buttonCount * rowHeight + std::max(0, buttonCount - 1) * rowSpacing > rect.height) {
|
|
rowHeight = std::max(30, (rect.height - std::max(0, buttonCount - 1) * rowSpacing) / buttonCount);
|
|
}
|
|
if (buttonCount * rowHeight + std::max(0, buttonCount - 1) * rowSpacing > rect.height) {
|
|
rowHeight = std::max(1, rect.height / buttonCount);
|
|
rowSpacing = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < buttonCount; ++i) {
|
|
const int tileY = rect.y + static_cast<int>(i) * (rowHeight + rowSpacing);
|
|
|
|
const bool selected = selectedIndex == i;
|
|
|
|
if (selected) {
|
|
renderer.fillRect(rect.x + BaseMetrics::values.contentSidePadding, tileY,
|
|
rect.width - BaseMetrics::values.contentSidePadding * 2, rowHeight);
|
|
} else {
|
|
renderer.drawRect(rect.x + BaseMetrics::values.contentSidePadding, tileY,
|
|
rect.width - BaseMetrics::values.contentSidePadding * 2, rowHeight);
|
|
}
|
|
|
|
std::string labelStr = buttonLabel(i);
|
|
const char* label = labelStr.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 + (rowHeight - 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::drawStatusBar(GfxRenderer& renderer, const float bookProgress, const int currentPage,
|
|
const int pageCount, std::string title, const int paddingBottom,
|
|
const int textYOffset) const {
|
|
auto metrics = UITheme::getInstance().getMetrics();
|
|
int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft;
|
|
renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom,
|
|
&orientedMarginLeft);
|
|
|
|
// Draw Progress Text
|
|
const auto screenHeight = renderer.getScreenHeight();
|
|
auto textY = screenHeight - UITheme::getInstance().getStatusBarHeight() - orientedMarginBottom - paddingBottom - 4;
|
|
int progressTextWidth = 0;
|
|
|
|
if (SETTINGS.statusBarBookProgressPercentage || SETTINGS.statusBarChapterPageCount) {
|
|
// Right aligned text for progress counter
|
|
char progressStr[32];
|
|
|
|
if (SETTINGS.statusBarBookProgressPercentage && SETTINGS.statusBarChapterPageCount) {
|
|
snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", currentPage, pageCount, bookProgress);
|
|
} else if (SETTINGS.statusBarBookProgressPercentage) {
|
|
snprintf(progressStr, sizeof(progressStr), "%.0f%%", bookProgress);
|
|
} else {
|
|
snprintf(progressStr, sizeof(progressStr), "%d/%d", currentPage, pageCount);
|
|
}
|
|
|
|
progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr);
|
|
renderer.drawText(
|
|
SMALL_FONT_ID,
|
|
renderer.getScreenWidth() - metrics.statusBarHorizontalMargin - orientedMarginRight - progressTextWidth, textY,
|
|
progressStr);
|
|
}
|
|
|
|
// Draw Progress Bar
|
|
if (SETTINGS.statusBarProgressBar != CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS) {
|
|
const int progressBarMaxWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight;
|
|
const int progressBarY = renderer.getScreenHeight() - orientedMarginBottom -
|
|
((SETTINGS.statusBarProgressBarThickness + 1) * 2) - paddingBottom;
|
|
size_t progress;
|
|
if (SETTINGS.statusBarProgressBar == CrossPointSettings::STATUS_BAR_PROGRESS_BAR::BOOK_PROGRESS) {
|
|
progress = static_cast<size_t>(bookProgress);
|
|
} else {
|
|
// Chapter progress
|
|
progress = (pageCount > 0) ? (static_cast<float>(currentPage) / pageCount) * 100 : 0;
|
|
}
|
|
const int barWidth = progressBarMaxWidth * progress / 100;
|
|
renderer.fillRect(orientedMarginLeft, progressBarY, barWidth, ((SETTINGS.statusBarProgressBarThickness + 1) * 2),
|
|
true);
|
|
}
|
|
|
|
// Draw Battery
|
|
const bool showBatteryPercentage =
|
|
SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER;
|
|
if (SETTINGS.statusBarBattery) {
|
|
GUI.drawBatteryLeft(renderer,
|
|
Rect{metrics.statusBarHorizontalMargin + orientedMarginLeft + 1, textY, metrics.batteryWidth,
|
|
metrics.batteryHeight},
|
|
showBatteryPercentage);
|
|
}
|
|
|
|
// Draw Clock
|
|
int clockTextWidth = 0;
|
|
if (SETTINGS.useClock && SETTINGS.statusBarClock) {
|
|
char clockStr[16];
|
|
HalClock::formatTime(clockStr, sizeof(clockStr), !SETTINGS.clockFormat12h);
|
|
clockTextWidth = renderer.getTextWidth(SMALL_FONT_ID, clockStr);
|
|
const int batterySize = SETTINGS.statusBarBattery ? (showBatteryPercentage ? 50 : 20) : 0;
|
|
renderer.drawText(SMALL_FONT_ID, metrics.statusBarHorizontalMargin + orientedMarginLeft + batterySize + 8, textY,
|
|
clockStr);
|
|
}
|
|
|
|
// Draw Title
|
|
if (!title.empty()) {
|
|
textY -= textYOffset;
|
|
// Centered chapter title text
|
|
// Page width minus existing content with 30px padding on each side
|
|
const int rendererableScreenWidth =
|
|
renderer.getScreenWidth() - (metrics.statusBarHorizontalMargin * 2) - orientedMarginLeft - orientedMarginRight;
|
|
|
|
const int batterySize = SETTINGS.statusBarBattery ? (showBatteryPercentage ? 50 : 20) : 0;
|
|
const int clockSize = clockTextWidth > 0 ? clockTextWidth + 8 : 0;
|
|
const int titleMarginLeft = batterySize + clockSize + 30;
|
|
const int titleMarginRight = progressTextWidth + 30;
|
|
|
|
// Attempt to center title on the screen, but if title is too wide then later we will center it within the
|
|
// available space.
|
|
int titleMarginLeftAdjusted = std::max(titleMarginLeft, titleMarginRight);
|
|
int availableTitleSpace = rendererableScreenWidth - 2 * titleMarginLeftAdjusted;
|
|
|
|
int titleWidth;
|
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
|
if (titleWidth > availableTitleSpace) {
|
|
// Not enough space to center on the screen, center it within the remaining space instead
|
|
availableTitleSpace = rendererableScreenWidth - titleMarginLeft - titleMarginRight;
|
|
titleMarginLeftAdjusted = titleMarginLeft;
|
|
}
|
|
if (titleWidth > availableTitleSpace) {
|
|
title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTitleSpace);
|
|
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str());
|
|
}
|
|
|
|
renderer.drawText(SMALL_FONT_ID,
|
|
titleMarginLeftAdjusted + metrics.statusBarHorizontalMargin + orientedMarginLeft +
|
|
(availableTitleSpace - titleWidth) / 2,
|
|
textY, title.c_str());
|
|
}
|
|
}
|
|
|
|
void BaseTheme::drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const {
|
|
const auto& metrics = UITheme::getInstance().getMetrics();
|
|
auto truncatedLabel =
|
|
renderer.truncatedText(SMALL_FONT_ID, label, rect.width - metrics.contentSidePadding * 2, EpdFontFamily::REGULAR);
|
|
renderer.drawCenteredText(SMALL_FONT_ID, rect.y, truncatedLabel.c_str());
|
|
}
|
|
|
|
void BaseTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const {
|
|
renderer.drawText(UI_12_FONT_ID, rect.x + 10, rect.y, "[");
|
|
renderer.drawText(UI_12_FONT_ID, rect.x + rect.width - 15, rect.y + rect.height, "]");
|
|
}
|
|
|
|
void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label,
|
|
const bool isSelected) const {
|
|
const int itemWidth = renderer.getTextWidth(UI_10_FONT_ID, label);
|
|
const int textX = rect.x + (rect.width - itemWidth) / 2;
|
|
if (isSelected) {
|
|
renderer.drawText(UI_10_FONT_ID, textX - 6, rect.y, "[");
|
|
renderer.drawText(UI_10_FONT_ID, textX + itemWidth, rect.y, "]");
|
|
}
|
|
renderer.drawText(UI_10_FONT_ID, textX, rect.y, label);
|
|
}
|