feat: Lyra screens (#732)
## 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>
This commit is contained in:
@@ -19,6 +19,7 @@ namespace {
|
||||
constexpr int batteryPercentSpacing = 4;
|
||||
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) {
|
||||
@@ -87,6 +88,7 @@ void BaseTheme::drawProgressBar(const GfxRenderer& renderer, Rect rect, const si
|
||||
// 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);
|
||||
|
||||
@@ -185,7 +187,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
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) const {
|
||||
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;
|
||||
@@ -251,7 +253,12 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
}
|
||||
}
|
||||
|
||||
void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const {
|
||||
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
|
||||
@@ -267,6 +274,36 @@ void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
||||
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,
|
||||
@@ -668,3 +705,26 @@ void BaseTheme::drawReadingProgressBar(const GfxRenderer& renderer, const size_t
|
||||
const int barWidth = progressBarMaxWidth * bookProgress / 100;
|
||||
renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BaseMetrics::values.bookProgressBarHeight, true);
|
||||
}
|
||||
|
||||
void BaseTheme::drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) 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);
|
||||
}
|
||||
@@ -51,10 +51,14 @@ struct ThemeMetrics {
|
||||
int buttonHintsHeight;
|
||||
int sideButtonHintsWidth;
|
||||
|
||||
int versionTextRightX;
|
||||
int versionTextY;
|
||||
|
||||
int progressBarHeight;
|
||||
int bookProgressBarHeight;
|
||||
|
||||
int keyboardKeyWidth;
|
||||
int keyboardKeyHeight;
|
||||
int keyboardKeySpacing;
|
||||
bool keyboardBottomAligned;
|
||||
bool keyboardCenteredText;
|
||||
};
|
||||
|
||||
// Default theme implementation (Classic Theme)
|
||||
@@ -82,9 +86,13 @@ constexpr ThemeMetrics values = {.batteryWidth = 15,
|
||||
.homeRecentBooksCount = 1,
|
||||
.buttonHintsHeight = 40,
|
||||
.sideButtonHintsWidth = 30,
|
||||
.versionTextRightX = 20,
|
||||
.versionTextY = 738,
|
||||
.bookProgressBarHeight = 4};
|
||||
.progressBarHeight = 16,
|
||||
.bookProgressBarHeight = 4,
|
||||
.keyboardKeyWidth = 18,
|
||||
.keyboardKeyHeight = 18,
|
||||
.keyboardKeySpacing = 3,
|
||||
.keyboardBottomAligned = false,
|
||||
.keyboardCenteredText = false};
|
||||
}
|
||||
|
||||
class BaseTheme {
|
||||
@@ -102,11 +110,14 @@ class BaseTheme {
|
||||
virtual void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const;
|
||||
virtual void 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) const;
|
||||
|
||||
virtual void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const;
|
||||
const std::function<std::string(int index)>& rowSubtitle = nullptr,
|
||||
const std::function<std::string(int index)>& rowIcon = nullptr,
|
||||
const std::function<std::string(int index)>& rowValue = nullptr,
|
||||
bool highlightValue = false) const;
|
||||
virtual void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title,
|
||||
const char* subtitle = nullptr) const;
|
||||
virtual void drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label,
|
||||
const char* rightLabel = nullptr) const;
|
||||
virtual void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs,
|
||||
bool selected) const;
|
||||
virtual void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
||||
@@ -118,4 +129,7 @@ class BaseTheme {
|
||||
virtual Rect drawPopup(const GfxRenderer& renderer, const char* message) const;
|
||||
virtual void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const;
|
||||
virtual void drawReadingProgressBar(const GfxRenderer& renderer, const size_t bookProgress) const;
|
||||
virtual void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const;
|
||||
virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const;
|
||||
virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const;
|
||||
};
|
||||
104
src/components/themes/lyra/Lyra3CoversTheme.cpp
Normal file
104
src/components/themes/lyra/Lyra3CoversTheme.cpp
Normal file
@@ -0,0 +1,104 @@
|
||||
#include "Lyra3CoversTheme.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.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 hPaddingInSelection = 8;
|
||||
constexpr int cornerRadius = 6;
|
||||
int coverWidth = 0;
|
||||
} // namespace
|
||||
|
||||
void Lyra3CoversTheme::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 * Lyra3CoversMetrics::values.contentSidePadding) / 3;
|
||||
const int tileHeight = rect.height;
|
||||
const int bookTitleHeight = tileHeight - Lyra3CoversMetrics::values.homeCoverHeight - hPaddingInSelection;
|
||||
const int tileY = rect.y;
|
||||
const bool hasContinueReading = !recentBooks.empty();
|
||||
|
||||
// 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) {
|
||||
if (!coverRendered) {
|
||||
for (int i = 0;
|
||||
i < std::min(static_cast<int>(recentBooks.size()), Lyra3CoversMetrics::values.homeRecentBooksCount); i++) {
|
||||
std::string coverPath = recentBooks[i].coverBmpPath;
|
||||
bool hasCover = true;
|
||||
int tileX = Lyra3CoversMetrics::values.contentSidePadding + tileWidth * i;
|
||||
if (coverPath.empty()) {
|
||||
hasCover = false;
|
||||
} else {
|
||||
const std::string coverBmpPath =
|
||||
UITheme::getCoverThumbPath(coverPath, Lyra3CoversMetrics::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) {
|
||||
float coverHeight = static_cast<float>(bitmap.getHeight());
|
||||
float coverWidth = static_cast<float>(bitmap.getWidth());
|
||||
float ratio = coverWidth / coverHeight;
|
||||
const float tileRatio = static_cast<float>(tileWidth - 2 * hPaddingInSelection) /
|
||||
static_cast<float>(Lyra3CoversMetrics::values.homeCoverHeight);
|
||||
float cropX = 1.0f - (tileRatio / ratio);
|
||||
|
||||
renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection,
|
||||
tileWidth - 2 * hPaddingInSelection, Lyra3CoversMetrics::values.homeCoverHeight,
|
||||
cropX);
|
||||
} else {
|
||||
hasCover = false;
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasCover) {
|
||||
renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection,
|
||||
tileWidth - 2 * hPaddingInSelection, Lyra3CoversMetrics::values.homeCoverHeight);
|
||||
}
|
||||
}
|
||||
|
||||
coverBufferStored = storeCoverBuffer();
|
||||
coverRendered = true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < std::min(static_cast<int>(recentBooks.size()), Lyra3CoversMetrics::values.homeRecentBooksCount);
|
||||
i++) {
|
||||
bool bookSelected = (selectorIndex == i);
|
||||
|
||||
int tileX = Lyra3CoversMetrics::values.contentSidePadding + tileWidth * i;
|
||||
auto title =
|
||||
renderer.truncatedText(UI_10_FONT_ID, recentBooks[i].title.c_str(), tileWidth - 2 * hPaddingInSelection);
|
||||
|
||||
if (bookSelected) {
|
||||
// Draw selection box
|
||||
renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false,
|
||||
Color::LightGray);
|
||||
renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection,
|
||||
Lyra3CoversMetrics::values.homeCoverHeight, Color::LightGray);
|
||||
renderer.fillRectDither(tileX + tileWidth - hPaddingInSelection, tileY + hPaddingInSelection,
|
||||
hPaddingInSelection, Lyra3CoversMetrics::values.homeCoverHeight, Color::LightGray);
|
||||
renderer.fillRoundedRect(tileX, tileY + Lyra3CoversMetrics::values.homeCoverHeight + hPaddingInSelection,
|
||||
tileWidth, bookTitleHeight, cornerRadius, false, false, true, true, Color::LightGray);
|
||||
}
|
||||
renderer.drawText(UI_10_FONT_ID, tileX + hPaddingInSelection,
|
||||
tileY + tileHeight - bookTitleHeight + hPaddingInSelection + 5, title.c_str(), true);
|
||||
}
|
||||
} else {
|
||||
drawEmptyRecents(renderer, rect);
|
||||
}
|
||||
}
|
||||
41
src/components/themes/lyra/Lyra3CoversTheme.h
Normal file
41
src/components/themes/lyra/Lyra3CoversTheme.h
Normal file
@@ -0,0 +1,41 @@
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "components/themes/lyra/LyraTheme.h"
|
||||
|
||||
class GfxRenderer;
|
||||
|
||||
// Lyra theme metrics (zero runtime cost)
|
||||
namespace Lyra3CoversMetrics {
|
||||
constexpr ThemeMetrics values = {.batteryWidth = 16,
|
||||
.batteryHeight = 12,
|
||||
.topPadding = 5,
|
||||
.batteryBarHeight = 40,
|
||||
.headerHeight = 84,
|
||||
.verticalSpacing = 16,
|
||||
.contentSidePadding = 20,
|
||||
.listRowHeight = 40,
|
||||
.listWithSubtitleRowHeight = 60,
|
||||
.menuRowHeight = 64,
|
||||
.menuSpacing = 8,
|
||||
.tabSpacing = 8,
|
||||
.tabBarHeight = 40,
|
||||
.scrollBarWidth = 4,
|
||||
.scrollBarRightOffset = 5,
|
||||
.homeTopPadding = 56,
|
||||
.homeCoverHeight = 226,
|
||||
.homeCoverTileHeight = 287,
|
||||
.homeRecentBooksCount = 3,
|
||||
.buttonHintsHeight = 40,
|
||||
.sideButtonHintsWidth = 30,
|
||||
.progressBarHeight = 16,
|
||||
.bookProgressBarHeight = 4};
|
||||
}
|
||||
|
||||
class Lyra3CoversTheme : public LyraTheme {
|
||||
public:
|
||||
void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
||||
const int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored,
|
||||
std::function<bool()> storeCoverBuffer) const override;
|
||||
};
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
@@ -20,6 +21,9 @@ 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 {
|
||||
@@ -101,7 +105,7 @@ void LyraTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const b
|
||||
}
|
||||
}
|
||||
|
||||
void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const {
|
||||
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 =
|
||||
@@ -112,14 +116,43 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
||||
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, rect.width - LyraMetrics::values.contentSidePadding * 2, EpdFontFamily::BOLD);
|
||||
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,
|
||||
@@ -158,7 +191,7 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
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) const {
|
||||
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;
|
||||
@@ -193,8 +226,14 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
const int itemY = rect.y + (i % pageItems) * rowHeight;
|
||||
|
||||
// Draw name
|
||||
int textWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2 -
|
||||
(rowValue != nullptr ? 60 : 0); // TODO truncate according to value width?
|
||||
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,
|
||||
@@ -208,22 +247,16 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
itemY + 30, subtitle.c_str(), true);
|
||||
}
|
||||
|
||||
if (rowValue != nullptr) {
|
||||
// Draw value
|
||||
std::string valueText = rowValue(i);
|
||||
if (!valueText.empty()) {
|
||||
const auto valueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str());
|
||||
|
||||
if (i == selectedIndex) {
|
||||
renderer.fillRoundedRect(
|
||||
contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection * 2 - valueTextWidth, itemY,
|
||||
valueTextWidth + hPaddingInSelection * 2, rowHeight, cornerRadius, Color::Black);
|
||||
}
|
||||
|
||||
renderer.drawText(UI_10_FONT_ID,
|
||||
contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - valueTextWidth,
|
||||
itemY + 6, valueText.c_str(), i != selectedIndex);
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,82 +335,98 @@ void LyraTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* top
|
||||
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) / 3;
|
||||
const int tileWidth = rect.width - 2 * LyraMetrics::values.contentSidePadding;
|
||||
const int tileHeight = rect.height;
|
||||
const int bookTitleHeight = tileHeight - LyraMetrics::values.homeCoverHeight - hPaddingInSelection;
|
||||
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) {
|
||||
for (int i = 0; i < std::min(static_cast<int>(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount);
|
||||
i++) {
|
||||
std::string coverPath = recentBooks[i].coverBmpPath;
|
||||
int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i;
|
||||
renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, tileWidth - 2 * hPaddingInSelection,
|
||||
LyraMetrics::values.homeCoverHeight);
|
||||
if (!coverPath.empty()) {
|
||||
const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::values.homeCoverHeight);
|
||||
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) {
|
||||
float coverHeight = static_cast<float>(bitmap.getHeight());
|
||||
float coverWidth = static_cast<float>(bitmap.getWidth());
|
||||
float ratio = coverWidth / coverHeight;
|
||||
const float tileRatio = static_cast<float>(tileWidth - 2 * hPaddingInSelection) /
|
||||
static_cast<float>(LyraMetrics::values.homeCoverHeight);
|
||||
float cropX = 1.0f - (tileRatio / ratio);
|
||||
renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection,
|
||||
tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight, cropX);
|
||||
}
|
||||
file.close();
|
||||
// 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;
|
||||
}
|
||||
|
||||
for (int i = 0; i < std::min(static_cast<int>(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount); i++) {
|
||||
bool bookSelected = (selectorIndex == i);
|
||||
bool bookSelected = (selectorIndex == 0);
|
||||
|
||||
int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i;
|
||||
auto title =
|
||||
renderer.truncatedText(UI_10_FONT_ID, recentBooks[i].title.c_str(), tileWidth - 2 * hPaddingInSelection);
|
||||
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 + tileWidth - hPaddingInSelection, tileY + hPaddingInSelection,
|
||||
hPaddingInSelection, LyraMetrics::values.homeCoverHeight, Color::LightGray);
|
||||
renderer.fillRoundedRect(tileX, tileY + LyraMetrics::values.homeCoverHeight + hPaddingInSelection, tileWidth,
|
||||
bookTitleHeight, cornerRadius, false, false, true, true, Color::LightGray);
|
||||
}
|
||||
renderer.drawText(UI_10_FONT_ID, tileX + hPaddingInSelection,
|
||||
tileY + tileHeight - bookTitleHeight + hPaddingInSelection + 5, title.c_str(), true);
|
||||
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 - LyraMetrics::values.menuSpacing) / 2;
|
||||
Rect tileRect =
|
||||
Rect{rect.x + LyraMetrics::values.contentSidePadding + (LyraMetrics::values.menuSpacing + tileWidth) * (i % 2),
|
||||
rect.y + static_cast<int>(i / 2) * (LyraMetrics::values.menuRowHeight + LyraMetrics::values.menuSpacing),
|
||||
tileWidth, LyraMetrics::values.menuRowHeight};
|
||||
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;
|
||||
|
||||
@@ -432,3 +481,21 @@ void LyraTheme::fillPopupProgress(const GfxRenderer& renderer, const Rect& layou
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -23,13 +23,17 @@ constexpr ThemeMetrics values = {.batteryWidth = 16,
|
||||
.scrollBarRightOffset = 5,
|
||||
.homeTopPadding = 56,
|
||||
.homeCoverHeight = 226,
|
||||
.homeCoverTileHeight = 287,
|
||||
.homeRecentBooksCount = 3,
|
||||
.homeCoverTileHeight = 242,
|
||||
.homeRecentBooksCount = 1,
|
||||
.buttonHintsHeight = 40,
|
||||
.sideButtonHintsWidth = 30,
|
||||
.versionTextRightX = 20,
|
||||
.versionTextY = 55,
|
||||
.bookProgressBarHeight = 4};
|
||||
.progressBarHeight = 16,
|
||||
.bookProgressBarHeight = 4,
|
||||
.keyboardKeyWidth = 31,
|
||||
.keyboardKeyHeight = 50,
|
||||
.keyboardKeySpacing = 0,
|
||||
.keyboardBottomAligned = true,
|
||||
.keyboardCenteredText = true};
|
||||
}
|
||||
|
||||
class LyraTheme : public BaseTheme {
|
||||
@@ -38,14 +42,16 @@ class LyraTheme : public BaseTheme {
|
||||
// void drawProgressBar(const GfxRenderer& renderer, Rect rect, size_t current, size_t total) override;
|
||||
void drawBatteryLeft(const GfxRenderer& renderer, Rect rect, bool showPercentage = true) const override;
|
||||
void drawBatteryRight(const GfxRenderer& renderer, Rect rect, bool showPercentage = true) const override;
|
||||
void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const override;
|
||||
void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const override;
|
||||
void drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label,
|
||||
const char* rightLabel = nullptr) const override;
|
||||
void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs,
|
||||
bool selected) const override;
|
||||
void 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) const override;
|
||||
const std::function<std::string(int index)>& rowValue, bool highlightValue) const override;
|
||||
void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3,
|
||||
const char* btn4) const override;
|
||||
void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const override;
|
||||
@@ -55,6 +61,9 @@ class LyraTheme : public BaseTheme {
|
||||
void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
||||
const int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored,
|
||||
std::function<bool()> storeCoverBuffer) const override;
|
||||
void drawEmptyRecents(const GfxRenderer& renderer, const Rect rect) const;
|
||||
Rect drawPopup(const GfxRenderer& renderer, const char* message) const override;
|
||||
void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const override;
|
||||
void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const override;
|
||||
void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const override;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user