## Summary Implements Lyra theme for some more Crosspoint screens:       ## Additional Context - A bit of refactoring for list scrolling logic --- ### AI Usage 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>
502 lines
23 KiB
C++
502 lines
23 KiB
C++
#include "LyraTheme.h"
|
|
|
|
#include <GfxRenderer.h>
|
|
#include <HalStorage.h>
|
|
#include <I18n.h>
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
|
|
#include "Battery.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;
|
|
constexpr int popupMarginX = 16;
|
|
constexpr int popupMarginY = 12;
|
|
constexpr int maxSubtitleWidth = 100;
|
|
constexpr int maxListValueWidth = 200;
|
|
int coverWidth = 0;
|
|
} // 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 char* subtitle) 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);
|
|
|
|
int maxTitleWidth =
|
|
rect.width - LyraMetrics::values.contentSidePadding * 2 - (subtitle != nullptr ? maxSubtitleWidth : 0);
|
|
|
|
if (title) {
|
|
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,
|
|
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<std::string(int index)>& rowTitle,
|
|
const std::function<std::string(int index)>& rowSubtitle,
|
|
const std::function<std::string(int index)>& rowIcon,
|
|
const std::function<std::string(int index)>& rowValue, bool highlightValue) const {
|
|
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 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,
|
|
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);
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
}
|
|
}
|
|
|
|
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<RecentBook>& recentBooks,
|
|
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
|
bool& bufferRestored, std::function<bool()> storeCoverBuffer) const {
|
|
const int tileWidth = rect.width - 2 * LyraMetrics::values.contentSidePadding;
|
|
const int tileHeight = rect.height;
|
|
const int tileY = rect.y;
|
|
const bool hasContinueReading = !recentBooks.empty();
|
|
if (coverWidth == 0) {
|
|
coverWidth = LyraMetrics::values.homeCoverHeight * 0.6;
|
|
}
|
|
|
|
// 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) {
|
|
RecentBook book = recentBooks[0];
|
|
if (!coverRendered) {
|
|
std::string coverPath = book.coverBmpPath;
|
|
bool hasCover = true;
|
|
int tileX = LyraMetrics::values.contentSidePadding;
|
|
if (coverPath.empty()) {
|
|
hasCover = false;
|
|
} else {
|
|
const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::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) {
|
|
coverWidth = bitmap.getWidth();
|
|
renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth,
|
|
LyraMetrics::values.homeCoverHeight);
|
|
} else {
|
|
hasCover = false;
|
|
}
|
|
file.close();
|
|
}
|
|
}
|
|
|
|
if (!hasCover) {
|
|
renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth,
|
|
LyraMetrics::values.homeCoverHeight);
|
|
}
|
|
|
|
coverBufferStored = storeCoverBuffer();
|
|
coverRendered = true;
|
|
}
|
|
|
|
bool bookSelected = (selectorIndex == 0);
|
|
|
|
int tileX = LyraMetrics::values.contentSidePadding;
|
|
int textWidth = tileWidth - 2 * hPaddingInSelection - LyraMetrics::values.verticalSpacing - coverWidth;
|
|
|
|
if (bookSelected) {
|
|
// Draw selection box
|
|
renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false,
|
|
Color::LightGray);
|
|
renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection,
|
|
LyraMetrics::values.homeCoverHeight, Color::LightGray);
|
|
renderer.fillRectDither(tileX + hPaddingInSelection + coverWidth, tileY + hPaddingInSelection,
|
|
tileWidth - hPaddingInSelection - coverWidth, LyraMetrics::values.homeCoverHeight,
|
|
Color::LightGray);
|
|
renderer.fillRoundedRect(tileX, tileY + LyraMetrics::values.homeCoverHeight + hPaddingInSelection, tileWidth,
|
|
hPaddingInSelection, cornerRadius, false, false, true, true, Color::LightGray);
|
|
}
|
|
|
|
auto title = renderer.truncatedText(UI_12_FONT_ID, book.title.c_str(), textWidth, EpdFontFamily::BOLD);
|
|
auto author = renderer.truncatedText(UI_10_FONT_ID, book.author.c_str(), textWidth);
|
|
auto bookTitleHeight = renderer.getTextHeight(UI_12_FONT_ID);
|
|
renderer.drawText(UI_12_FONT_ID, tileX + hPaddingInSelection + coverWidth + LyraMetrics::values.verticalSpacing,
|
|
tileY + tileHeight / 2 - bookTitleHeight, title.c_str(), true, EpdFontFamily::BOLD);
|
|
renderer.drawText(UI_10_FONT_ID, tileX + hPaddingInSelection + coverWidth + LyraMetrics::values.verticalSpacing,
|
|
tileY + tileHeight / 2 + 5, author.c_str(), true);
|
|
} else {
|
|
drawEmptyRecents(renderer, rect);
|
|
}
|
|
}
|
|
|
|
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;
|
|
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;
|
|
|
|
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 y = 132;
|
|
constexpr int outline = 2;
|
|
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 + popupMarginX * 2;
|
|
const int h = textHeight + popupMarginY * 2;
|
|
const int x = (renderer.getScreenWidth() - w) / 2;
|
|
|
|
renderer.fillRoundedRect(x - outline, y - outline, w + outline * 2, h + outline * 2, cornerRadius + outline,
|
|
Color::White);
|
|
renderer.fillRoundedRect(x, y, w, h, cornerRadius, Color::Black);
|
|
|
|
const int textX = x + (w - textWidth) / 2;
|
|
const int textY = y + popupMarginY - 2;
|
|
renderer.drawText(UI_12_FONT_ID, textX, textY, message, false, 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;
|
|
|
|
// Twice the margin in drawPopup to match text width
|
|
const int barWidth = layout.width - popupMarginX * 2;
|
|
const int barX = layout.x + (layout.width - barWidth) / 2;
|
|
// Center inside the margin of drawPopup. The - 1 is added to account for the - 2 in drawPopup.
|
|
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);
|
|
}
|