New BmpViewerActivity opens, parses, and renders BMP files with centered aspect-ratio-preserving display and localized back navigation. Library file filter extended to include .bmp. ReaderActivity routes BMP paths to the new viewer. LyraTheme button hint backgrounds switched to rounded rect fills to prevent overflow artifacts. Co-authored-by: Cursor <cursoragent@cursor.com>
911 lines
39 KiB
C++
911 lines
39 KiB
C++
#include "LyraTheme.h"
|
|
|
|
#include <GfxRenderer.h>
|
|
#include <HalPowerManager.h>
|
|
#include <HalStorage.h>
|
|
#include <I18n.h>
|
|
#include <Utf8.h>
|
|
|
|
#include <cstdint>
|
|
#include <ctime>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "CrossPointSettings.h"
|
|
#include "RecentBooksStore.h"
|
|
#include "components/UITheme.h"
|
|
#include "components/icons/book.h"
|
|
#include "components/icons/book24.h"
|
|
#include "components/icons/cover.h"
|
|
#include "components/icons/file24.h"
|
|
#include "components/icons/folder.h"
|
|
#include "components/icons/folder24.h"
|
|
#include "components/icons/hotspot.h"
|
|
#include "components/icons/image24.h"
|
|
#include "components/icons/library.h"
|
|
#include "components/icons/recent.h"
|
|
#include "components/icons/settings2.h"
|
|
#include "components/icons/text24.h"
|
|
#include "components/icons/transfer.h"
|
|
#include "components/icons/wifi.h"
|
|
#include "fontIds.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;
|
|
constexpr int mainMenuIconSize = 32;
|
|
constexpr int listIconSize = 24;
|
|
constexpr int mainMenuColumns = 2;
|
|
|
|
const uint8_t* iconForName(UIIcon icon, int size) {
|
|
if (size == 24) {
|
|
switch (icon) {
|
|
case UIIcon::Folder:
|
|
return Folder24Icon;
|
|
case UIIcon::Text:
|
|
return Text24Icon;
|
|
case UIIcon::Image:
|
|
return Image24Icon;
|
|
case UIIcon::Book:
|
|
return Book24Icon;
|
|
case UIIcon::File:
|
|
return File24Icon;
|
|
default:
|
|
return nullptr;
|
|
}
|
|
} else if (size == 32) {
|
|
switch (icon) {
|
|
case UIIcon::Folder:
|
|
return FolderIcon;
|
|
case UIIcon::Book:
|
|
return BookIcon;
|
|
case UIIcon::Recent:
|
|
return RecentIcon;
|
|
case UIIcon::Settings:
|
|
return Settings2Icon;
|
|
case UIIcon::Transfer:
|
|
return TransferIcon;
|
|
case UIIcon::Library:
|
|
return LibraryIcon;
|
|
case UIIcon::Wifi:
|
|
return WifiIcon;
|
|
case UIIcon::Hotspot:
|
|
return HotspotIcon;
|
|
default:
|
|
return nullptr;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
std::string truncateWithEllipsis(const GfxRenderer& renderer, int fontId, const std::string& text, int maxWidth) {
|
|
std::string truncated = text;
|
|
std::string withEllipsis = truncated + "...";
|
|
while (!truncated.empty() && renderer.getTextWidth(fontId, withEllipsis.c_str()) > maxWidth) {
|
|
utf8RemoveLastChar(truncated);
|
|
withEllipsis = truncated + "...";
|
|
}
|
|
return truncated.empty() ? std::string("...") : withEllipsis;
|
|
}
|
|
|
|
std::vector<std::string> wrapTextToLines(const GfxRenderer& renderer, int fontId, const std::string& text, int maxWidth,
|
|
int maxLines) {
|
|
std::vector<std::string> lines;
|
|
if (text.empty() || maxWidth <= 0 || maxLines <= 0) return lines;
|
|
|
|
if (renderer.getTextWidth(fontId, text.c_str()) <= maxWidth) {
|
|
lines.push_back(text);
|
|
return lines;
|
|
}
|
|
|
|
if (maxLines == 1) {
|
|
lines.push_back(truncateWithEllipsis(renderer, fontId, text, maxWidth));
|
|
return lines;
|
|
}
|
|
|
|
static const char* const preferredDelimiters[] = {" -- ", " - ", " \xe2\x80\x93 ", " \xe2\x80\x94 "};
|
|
for (const char* delim : preferredDelimiters) {
|
|
size_t delimLen = strlen(delim);
|
|
auto pos = text.rfind(delim);
|
|
if (pos != std::string::npos && pos > 0) {
|
|
std::string firstPart = text.substr(0, pos);
|
|
if (renderer.getTextWidth(fontId, firstPart.c_str()) <= maxWidth) {
|
|
lines.push_back(firstPart);
|
|
std::string remainder = text.substr(pos + delimLen);
|
|
if (renderer.getTextWidth(fontId, remainder.c_str()) > maxWidth) {
|
|
lines.push_back(truncateWithEllipsis(renderer, fontId, remainder, maxWidth));
|
|
} else {
|
|
lines.push_back(remainder);
|
|
}
|
|
return lines;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string currentLine;
|
|
const unsigned char* ptr = reinterpret_cast<const unsigned char*>(text.c_str());
|
|
std::string lineAtBreak;
|
|
const unsigned char* ptrAtBreak = nullptr;
|
|
|
|
while (*ptr != 0) {
|
|
const unsigned char* charStart = ptr;
|
|
uint32_t cp = utf8NextCodepoint(&ptr);
|
|
std::string nextChar(reinterpret_cast<const char*>(charStart), static_cast<size_t>(ptr - charStart));
|
|
std::string candidate = currentLine + nextChar;
|
|
|
|
if (renderer.getTextWidth(fontId, candidate.c_str()) <= maxWidth) {
|
|
currentLine = candidate;
|
|
if (cp == ' ' || cp == '-') {
|
|
lineAtBreak = currentLine;
|
|
ptrAtBreak = ptr;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (static_cast<int>(lines.size()) < maxLines - 1 && !currentLine.empty()) {
|
|
if (ptrAtBreak != nullptr) {
|
|
std::string line = lineAtBreak;
|
|
while (!line.empty() && line.back() == ' ') line.pop_back();
|
|
lines.push_back(line);
|
|
ptr = ptrAtBreak;
|
|
while (*ptr == ' ') ++ptr;
|
|
currentLine.clear();
|
|
} else {
|
|
lines.push_back(currentLine);
|
|
currentLine = nextChar;
|
|
}
|
|
lineAtBreak.clear();
|
|
ptrAtBreak = nullptr;
|
|
} else {
|
|
lines.push_back(truncateWithEllipsis(renderer, fontId, currentLine, maxWidth));
|
|
return lines;
|
|
}
|
|
}
|
|
|
|
if (!currentLine.empty()) {
|
|
lines.push_back(currentLine);
|
|
}
|
|
return lines;
|
|
}
|
|
} // 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 = powerManager.getBatteryPercentage();
|
|
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 = powerManager.getBatteryPercentage();
|
|
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);
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
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 - 1, 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 - 1, 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 - 1, 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<UIIcon(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;
|
|
|
|
// Detect if selected row's title overflows and needs 2-line expansion
|
|
bool selectedExpands = false;
|
|
if (selectedIndex >= 0 && rowSubtitle == nullptr && rowValue != nullptr) {
|
|
int prelTotalPages = (itemCount + pageItems - 1) / pageItems;
|
|
int prelContentWidth =
|
|
rect.width -
|
|
(prelTotalPages > 1 ? (LyraMetrics::values.scrollBarWidth + LyraMetrics::values.scrollBarRightOffset) : 1);
|
|
int prelTextWidth = prelContentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2;
|
|
if (rowIcon != nullptr) prelTextWidth -= listIconSize + hPaddingInSelection;
|
|
|
|
auto selTitle = rowTitle(selectedIndex);
|
|
auto selValue = rowValue(selectedIndex);
|
|
int selValueWidth = 0;
|
|
if (!selValue.empty()) {
|
|
selValue = renderer.truncatedText(UI_10_FONT_ID, selValue.c_str(), maxListValueWidth);
|
|
selValueWidth = renderer.getTextWidth(UI_10_FONT_ID, selValue.c_str()) + hPaddingInSelection;
|
|
}
|
|
if (renderer.getTextWidth(UI_10_FONT_ID, selTitle.c_str()) > prelTextWidth - selValueWidth) {
|
|
selectedExpands = true;
|
|
}
|
|
}
|
|
|
|
const int effectivePageItems = selectedExpands ? std::max(1, pageItems - 1) : pageItems;
|
|
const int totalPages = (itemCount + effectivePageItems - 1) / effectivePageItems;
|
|
if (totalPages > 1) {
|
|
const int scrollAreaHeight = rect.height;
|
|
|
|
const int scrollBarHeight = (scrollAreaHeight * effectivePageItems) / itemCount;
|
|
const int currentPage = selectedIndex / effectivePageItems;
|
|
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);
|
|
}
|
|
|
|
int contentWidth =
|
|
rect.width -
|
|
(totalPages > 1 ? (LyraMetrics::values.scrollBarWidth + LyraMetrics::values.scrollBarRightOffset) : 1);
|
|
|
|
// Compute page start: use effective page items but prevent backward leak
|
|
int pageStartIndex;
|
|
if (selectedExpands) {
|
|
int rawStart = selectedIndex / effectivePageItems * effectivePageItems;
|
|
int originalStart = selectedIndex / pageItems * pageItems;
|
|
pageStartIndex = std::max(rawStart, originalStart);
|
|
if (selectedIndex >= pageStartIndex + effectivePageItems) {
|
|
pageStartIndex = selectedIndex - effectivePageItems + 1;
|
|
}
|
|
if (pageStartIndex > 0 && pageStartIndex == originalStart
|
|
&& selectedIndex < pageStartIndex + effectivePageItems - 1) {
|
|
int checkTextWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2;
|
|
if (rowIcon != nullptr) checkTextWidth -= listIconSize + hPaddingInSelection;
|
|
auto prevTitle = rowTitle(pageStartIndex - 1);
|
|
int prevValueWidth = 0;
|
|
if (rowValue != nullptr) {
|
|
auto prevValue = rowValue(pageStartIndex - 1);
|
|
prevValue = renderer.truncatedText(UI_10_FONT_ID, prevValue.c_str(), maxListValueWidth);
|
|
if (!prevValue.empty()) {
|
|
prevValueWidth = renderer.getTextWidth(UI_10_FONT_ID, prevValue.c_str()) + hPaddingInSelection;
|
|
}
|
|
}
|
|
if (renderer.getTextWidth(UI_10_FONT_ID, prevTitle.c_str()) > checkTextWidth - prevValueWidth) {
|
|
pageStartIndex--;
|
|
}
|
|
}
|
|
} else {
|
|
pageStartIndex = selectedIndex / pageItems * pageItems;
|
|
// Include previous page's boundary item if it would need expansion when selected,
|
|
// so it doesn't vanish when navigating from it to the current page.
|
|
if (pageStartIndex > 0 && selectedIndex < pageStartIndex + pageItems - 1) {
|
|
int checkTextWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2;
|
|
if (rowIcon != nullptr) checkTextWidth -= listIconSize + hPaddingInSelection;
|
|
auto prevTitle = rowTitle(pageStartIndex - 1);
|
|
int prevValueWidth = 0;
|
|
if (rowValue != nullptr) {
|
|
auto prevValue = rowValue(pageStartIndex - 1);
|
|
prevValue = renderer.truncatedText(UI_10_FONT_ID, prevValue.c_str(), maxListValueWidth);
|
|
if (!prevValue.empty()) {
|
|
prevValueWidth = renderer.getTextWidth(UI_10_FONT_ID, prevValue.c_str()) + hPaddingInSelection;
|
|
}
|
|
}
|
|
if (renderer.getTextWidth(UI_10_FONT_ID, prevTitle.c_str()) > checkTextWidth - prevValueWidth) {
|
|
pageStartIndex--;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw selection highlight
|
|
if (selectedIndex >= 0) {
|
|
int selRowsBeforeOnPage = selectedIndex - pageStartIndex;
|
|
int selY = rect.y + selRowsBeforeOnPage * rowHeight;
|
|
int selHeight = selectedExpands ? 2 * rowHeight : rowHeight;
|
|
renderer.fillRoundedRect(LyraMetrics::values.contentSidePadding, selY,
|
|
contentWidth - LyraMetrics::values.contentSidePadding * 2, selHeight, cornerRadius,
|
|
Color::LightGray);
|
|
}
|
|
|
|
int textX = rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection;
|
|
int textWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2;
|
|
int iconSize = listIconSize;
|
|
if (rowIcon != nullptr) {
|
|
iconSize = (rowSubtitle != nullptr) ? mainMenuIconSize : listIconSize;
|
|
textX += iconSize + hPaddingInSelection;
|
|
textWidth -= iconSize + hPaddingInSelection;
|
|
}
|
|
|
|
// Draw all items
|
|
int iconY = (rowSubtitle != nullptr) ? 16 : 10;
|
|
int yPos = rect.y;
|
|
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + effectivePageItems; i++) {
|
|
const bool isExpanded = (selectedExpands && i == selectedIndex);
|
|
|
|
int valueWidth = 0;
|
|
std::string valueText;
|
|
if (rowValue != nullptr) {
|
|
valueText = rowValue(i);
|
|
valueText = renderer.truncatedText(UI_10_FONT_ID, valueText.c_str(), maxListValueWidth);
|
|
if (!valueText.empty()) {
|
|
valueWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()) + hPaddingInSelection;
|
|
}
|
|
}
|
|
|
|
auto itemName = rowTitle(i);
|
|
|
|
if (isExpanded) {
|
|
int wrapWidth = textWidth;
|
|
auto lines = wrapTextToLines(renderer, UI_10_FONT_ID, itemName, wrapWidth, 2);
|
|
|
|
for (size_t l = 0; l < lines.size(); ++l) {
|
|
renderer.drawText(UI_10_FONT_ID, textX, yPos + 7 + static_cast<int>(l) * rowHeight, lines[l].c_str(), true);
|
|
}
|
|
|
|
if (rowIcon != nullptr) {
|
|
UIIcon icon = rowIcon(i);
|
|
const uint8_t* iconBitmap = iconForName(icon, iconSize);
|
|
if (iconBitmap != nullptr) {
|
|
renderer.drawIcon(iconBitmap, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection,
|
|
yPos + iconY, iconSize, iconSize);
|
|
}
|
|
}
|
|
|
|
if (!valueText.empty()) {
|
|
renderer.drawText(UI_10_FONT_ID,
|
|
rect.x + contentWidth - LyraMetrics::values.contentSidePadding - valueWidth,
|
|
yPos + rowHeight + 7, valueText.c_str(), true);
|
|
}
|
|
yPos += 2 * rowHeight;
|
|
} else {
|
|
int rowTextWidth = textWidth - valueWidth;
|
|
|
|
auto item = renderer.truncatedText(UI_10_FONT_ID, itemName.c_str(), rowTextWidth);
|
|
renderer.drawText(UI_10_FONT_ID, textX, yPos + 7, item.c_str(), true);
|
|
|
|
if (rowIcon != nullptr) {
|
|
UIIcon icon = rowIcon(i);
|
|
const uint8_t* iconBitmap = iconForName(icon, iconSize);
|
|
if (iconBitmap != nullptr) {
|
|
renderer.drawIcon(iconBitmap, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection,
|
|
yPos + iconY, iconSize, iconSize);
|
|
}
|
|
}
|
|
|
|
if (rowSubtitle != nullptr) {
|
|
std::string subtitleText = rowSubtitle(i);
|
|
auto subtitle = renderer.truncatedText(SMALL_FONT_ID, subtitleText.c_str(), rowTextWidth);
|
|
renderer.drawText(SMALL_FONT_ID, textX, yPos + 30, subtitle.c_str(), true);
|
|
}
|
|
|
|
if (!valueText.empty()) {
|
|
if (i == selectedIndex && highlightValue) {
|
|
renderer.fillRoundedRect(
|
|
contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - valueWidth, yPos,
|
|
valueWidth + hPaddingInSelection, rowHeight, cornerRadius, Color::Black);
|
|
}
|
|
renderer.drawText(UI_10_FONT_ID,
|
|
rect.x + contentWidth - LyraMetrics::values.contentSidePadding - valueWidth, yPos + 6,
|
|
valueText.c_str(), !(i == selectedIndex && highlightValue));
|
|
}
|
|
yPos += rowHeight;
|
|
}
|
|
}
|
|
}
|
|
|
|
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.fillRoundedRect(x, pageHeight - buttonY, buttonWidth, buttonHeight, cornerRadius, true, true, false,
|
|
false, Color::White);
|
|
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.fillRoundedRect(x, pageHeight - smallButtonHeight, buttonWidth, smallButtonHeight, cornerRadius, true,
|
|
true, false, false, Color::White);
|
|
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 bookCount = std::min(static_cast<int>(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount);
|
|
const int tileHeight = rect.height;
|
|
const int tileY = rect.y;
|
|
const int coverHeight = LyraMetrics::values.homeCoverHeight;
|
|
|
|
if (bookCount == 0) {
|
|
drawEmptyRecents(renderer, rect);
|
|
return;
|
|
}
|
|
|
|
auto wrapText = [&renderer](int fontId, const std::string& text, int maxWidth,
|
|
int maxLines) -> std::vector<std::string> {
|
|
std::vector<std::string> 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<std::string> lines;
|
|
std::string currentLine;
|
|
for (auto& word : words) {
|
|
if (static_cast<int>(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<int>(lines.size()) < maxLines) {
|
|
lines.push_back(currentLine);
|
|
}
|
|
return lines;
|
|
};
|
|
|
|
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<float>(bitmap.getWidth());
|
|
float bmpH = static_cast<float>(bitmap.getHeight());
|
|
float ratio = bmpW / bmpH;
|
|
int naturalWidth = static_cast<int>(coverHeight * ratio);
|
|
|
|
if (naturalWidth >= slotWidth) {
|
|
float slotRatio = static_cast<float>(slotWidth) / static_cast<float>(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) {
|
|
const bool bookSelected = (selectorIndex == 0);
|
|
const int cardX = LyraMetrics::values.contentSidePadding;
|
|
const int cardWidth = rect.width - 2 * LyraMetrics::values.contentSidePadding;
|
|
const int coverSlotWidth = static_cast<int>(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;
|
|
}
|
|
|
|
if (bookSelected) {
|
|
renderer.fillRoundedRect(cardX, tileY, cardWidth, hPaddingInSelection, cornerRadius, true, true, false, false,
|
|
Color::LightGray);
|
|
renderer.fillRectDither(cardX, tileY + hPaddingInSelection, hPaddingInSelection, coverHeight, Color::LightGray);
|
|
renderer.fillRectDither(cardX + cardWidth - hPaddingInSelection, tileY + hPaddingInSelection, hPaddingInSelection,
|
|
coverHeight, Color::LightGray);
|
|
renderer.fillRectDither(cardX + hPaddingInSelection + coverSlotWidth, tileY + hPaddingInSelection,
|
|
cardWidth - hPaddingInSelection * 2 - coverSlotWidth, coverHeight, Color::LightGray);
|
|
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);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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 {
|
|
const int tileWidth = (rect.width - 2 * LyraMetrics::values.contentSidePadding) / bookCount;
|
|
const int bottomSectionHeight = tileHeight - coverHeight - hPaddingInSelection;
|
|
|
|
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;
|
|
}
|
|
|
|
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) {
|
|
renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false,
|
|
Color::LightGray);
|
|
renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection, coverHeight, Color::LightGray);
|
|
renderer.fillRectDither(tileX + tileWidth - hPaddingInSelection, tileY + hPaddingInSelection,
|
|
hPaddingInSelection, coverHeight, Color::LightGray);
|
|
renderer.fillRoundedRect(tileX, tileY + coverHeight + hPaddingInSelection, tileWidth, bottomSectionHeight,
|
|
cornerRadius, false, false, true, true, Color::LightGray);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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::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<UIIcon(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();
|
|
int textX = tileRect.x + 16;
|
|
const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
|
|
const int textY = tileRect.y + (LyraMetrics::values.menuRowHeight - lineHeight) / 2;
|
|
|
|
if (rowIcon != nullptr) {
|
|
UIIcon icon = rowIcon(i);
|
|
const uint8_t* iconBitmap = iconForName(icon, mainMenuIconSize);
|
|
if (iconBitmap != nullptr) {
|
|
renderer.drawIcon(iconBitmap, textX, textY + 3, mainMenuIconSize, mainMenuIconSize);
|
|
textX += mainMenuIconSize + hPaddingInSelection + 2;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
const int barWidth = layout.width - popupMarginX * 2;
|
|
const int barX = layout.x + (layout.width - barWidth) / 2;
|
|
const int barY = layout.y + layout.height - popupMarginY / 2 - barHeight / 2 - 1;
|
|
|
|
int fillWidth = barWidth * progress / 100;
|
|
|
|
renderer.fillRect(barX, barY, fillWidth, barHeight, false);
|
|
|
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
}
|
|
|
|
void LyraTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const {
|
|
int lineY = rect.y + rect.height + renderer.getLineHeight(UI_12_FONT_ID) + LyraMetrics::values.verticalSpacing;
|
|
int lineW = textWidth + hPaddingInSelection * 2;
|
|
renderer.drawLine(rect.x + (rect.width - lineW) / 2, lineY, rect.x + (rect.width + lineW) / 2, lineY, 3);
|
|
}
|
|
|
|
void LyraTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label,
|
|
const bool isSelected) const {
|
|
if (isSelected) {
|
|
renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cornerRadius, Color::Black);
|
|
}
|
|
|
|
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, label);
|
|
const int textX = rect.x + (rect.width - textWidth) / 2;
|
|
const int textY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2;
|
|
renderer.drawText(UI_12_FONT_ID, textX, textY, label, !isSelected);
|
|
}
|