mod: improve home screen with adaptive layouts, clock, and set time

- 1-book view: horizontal layout with cover left, title/author right
- 2-3 book view: fix cover stretching by preserving aspect ratio
- 0-book view: show "Choose something to read" placeholder
- Selection highlight now fully contains title and author text
- Add optional clock display in home screen header (AM/PM or 24H)
- Add "Home Screen Clock" setting under Display
- Add "Set Time" activity for manual clock setting via Settings
- Increase homeCoverTileHeight to 310 for title/author breathing room

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
cottongin
2026-02-17 00:46:05 -05:00
parent 1d7971ae60
commit 7b3de29c59
19 changed files with 459 additions and 45 deletions

View File

@@ -138,6 +138,7 @@ uint8_t CrossPointSettings::writeSettings(FsFile& file, bool count_only) const {
// New fields added at end for backward compatibility
writer.writeItem(file, preferredPortrait);
writer.writeItem(file, preferredLandscape);
writer.writeItem(file, homeScreenClock);
return writer.item_count;
}
@@ -273,6 +274,8 @@ bool CrossPointSettings::loadFromFile() {
if (++settingsRead >= fileSettingsCount) break;
readAndValidate(inputFile, preferredLandscape, ORIENTATION_COUNT);
if (++settingsRead >= fileSettingsCount) break;
readAndValidate(inputFile, homeScreenClock, CLOCK_FORMAT_COUNT);
if (++settingsRead >= fileSettingsCount) break;
} while (false);
if (frontButtonMappingRead) {

View File

@@ -128,6 +128,9 @@ class CrossPointSettings {
// UI Theme
enum UI_THEME { CLASSIC = 0, LYRA = 1 };
// Home screen clock format
enum CLOCK_FORMAT { CLOCK_OFF = 0, CLOCK_AMPM = 1, CLOCK_24H = 2, CLOCK_FORMAT_COUNT };
// Sleep screen settings
uint8_t sleepScreen = DARK;
// Sleep screen cover mode settings
@@ -189,6 +192,9 @@ class CrossPointSettings {
uint8_t preferredPortrait = PORTRAIT;
uint8_t preferredLandscape = LANDSCAPE_CW;
// Home screen clock display format (OFF by default)
uint8_t homeScreenClock = CLOCK_OFF;
~CrossPointSettings() = default;
// Get singleton instance

View File

@@ -76,6 +76,9 @@ inline std::vector<SettingInfo> getSettingsList() {
{StrId::STR_THEME_CLASSIC, StrId::STR_THEME_LYRA}, "uiTheme", StrId::STR_CAT_DISPLAY),
SettingInfo::Toggle(StrId::STR_SUNLIGHT_FADING_FIX, &CrossPointSettings::fadingFix, "fadingFix",
StrId::STR_CAT_DISPLAY),
SettingInfo::Enum(StrId::STR_HOME_SCREEN_CLOCK, &CrossPointSettings::homeScreenClock,
{StrId::STR_STATE_OFF, StrId::STR_CLOCK_AMPM, StrId::STR_CLOCK_24H}, "homeScreenClock",
StrId::STR_CAT_DISPLAY),
// --- Reader ---
SettingInfo::DynamicEnum(

View File

@@ -9,7 +9,9 @@
#include <PlaceholderCoverGenerator.h>
#include <Xtc.h>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <vector>
#include "Battery.h"
@@ -238,6 +240,23 @@ void HomeActivity::render(Activity::RenderLock&&) {
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.homeTopPadding}, nullptr);
// Draw clock in the header area (left side) if enabled
if (SETTINGS.homeScreenClock != 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.homeScreenClock == 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");
}
renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, metrics.topPadding, timeBuf, true);
}
}
GUI.drawRecentBookCover(renderer, Rect{0, metrics.homeTopPadding, pageWidth, metrics.homeCoverTileHeight},
recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored,
std::bind(&HomeActivity::storeCoverBuffer, this));

View File

@@ -0,0 +1,157 @@
#include "SetTimeActivity.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include <cstdio>
#include <ctime>
#include <sys/time.h>
#include "CrossPointSettings.h"
#include "MappedInputManager.h"
#include "components/UITheme.h"
#include "fontIds.h"
void SetTimeActivity::onEnter() {
Activity::onEnter();
// Initialize from current system time if it's been set (year > 2000)
time_t now = time(nullptr);
struct tm* t = localtime(&now);
if (t != nullptr && t->tm_year > 100) {
hour = t->tm_hour;
minute = t->tm_min;
} else {
hour = 12;
minute = 0;
}
selectedField = 0;
requestUpdate();
}
void SetTimeActivity::onExit() { Activity::onExit(); }
void SetTimeActivity::loop() {
// Back button: discard and exit
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
onBack();
return;
}
// Confirm button: apply time and exit
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
applyTime();
onBack();
return;
}
// Left/Right: switch between hour and minute fields
if (mappedInput.wasReleased(MappedInputManager::Button::Left)) {
selectedField = 0;
requestUpdate();
return;
}
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
selectedField = 1;
requestUpdate();
return;
}
// Up/Down: increment/decrement the selected field
if (mappedInput.wasPressed(MappedInputManager::Button::Up)) {
if (selectedField == 0) {
hour = (hour + 1) % 24;
} else {
minute = (minute + 1) % 60;
}
requestUpdate();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
if (selectedField == 0) {
hour = (hour + 23) % 24;
} else {
minute = (minute + 59) % 60;
}
requestUpdate();
return;
}
}
void SetTimeActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
const int lineHeight12 = renderer.getLineHeight(UI_12_FONT_ID);
// Title
renderer.drawCenteredText(UI_12_FONT_ID, 20, tr(STR_SET_TIME), true, EpdFontFamily::BOLD);
// Format hour and minute strings
char hourStr[4];
char minuteStr[4];
snprintf(hourStr, sizeof(hourStr), "%02d", hour);
snprintf(minuteStr, sizeof(minuteStr), "%02d", minute);
const int colonWidth = renderer.getTextWidth(UI_12_FONT_ID, " : ");
const int digitWidth = renderer.getTextWidth(UI_12_FONT_ID, "00");
const int totalWidth = digitWidth * 2 + colonWidth;
const int startX = (pageWidth - totalWidth) / 2;
const int timeY = 80;
// Draw selection highlight behind the selected field
constexpr int highlightPad = 6;
if (selectedField == 0) {
renderer.fillRoundedRect(startX - highlightPad, timeY - 4, digitWidth + highlightPad * 2, lineHeight12 + 8, 6,
Color::LightGray);
} else {
renderer.fillRoundedRect(startX + digitWidth + colonWidth - highlightPad, timeY - 4,
digitWidth + highlightPad * 2, lineHeight12 + 8, 6, Color::LightGray);
}
// Draw the time digits and colon
renderer.drawText(UI_12_FONT_ID, startX, timeY, hourStr, true);
renderer.drawText(UI_12_FONT_ID, startX + digitWidth, timeY, " : ", true);
renderer.drawText(UI_12_FONT_ID, startX + digitWidth + colonWidth, timeY, minuteStr, true);
// Draw up/down arrows above and below the selected field
const int arrowX = (selectedField == 0) ? startX + digitWidth / 2 : startX + digitWidth + colonWidth + digitWidth / 2;
const int arrowUpY = timeY - 20;
const int arrowDownY = timeY + lineHeight12 + 12;
// Up arrow (simple triangle using lines)
constexpr int arrowSize = 6;
for (int row = 0; row < arrowSize; row++) {
renderer.drawLine(arrowX - row, arrowUpY + row, arrowX + row, arrowUpY + row);
}
// Down arrow
for (int row = 0; row < arrowSize; row++) {
renderer.drawLine(arrowX - row, arrowDownY + arrowSize - 1 - row, arrowX + row, arrowDownY + arrowSize - 1 - row);
}
// Button hints
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SAVE), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
}
void SetTimeActivity::applyTime() {
time_t now = time(nullptr);
struct tm newTime = {};
struct tm* current = localtime(&now);
if (current != nullptr && current->tm_year > 100) {
newTime = *current;
} else {
// If time was never set, use a reasonable date (2025-01-01)
newTime.tm_year = 125; // years since 1900
newTime.tm_mon = 0;
newTime.tm_mday = 1;
}
newTime.tm_hour = hour;
newTime.tm_min = minute;
newTime.tm_sec = 0;
time_t newEpoch = mktime(&newTime);
struct timeval tv = {.tv_sec = newEpoch, .tv_usec = 0};
settimeofday(&tv, nullptr);
}

View File

@@ -0,0 +1,27 @@
#pragma once
#include <functional>
#include "activities/Activity.h"
class SetTimeActivity final : public Activity {
public:
explicit SetTimeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack)
: Activity("SetTime", renderer, mappedInput), onBack(onBack) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
private:
const std::function<void()> onBack;
// 0 = editing hours, 1 = editing minutes
uint8_t selectedField = 0;
int hour = 12;
int minute = 0;
void applyTime();
};

View File

@@ -11,6 +11,7 @@
#include "LanguageSelectActivity.h"
#include "MappedInputManager.h"
#include "OtaUpdateActivity.h"
#include "SetTimeActivity.h"
#include "SettingsList.h"
#include "activities/network/WifiSelectionActivity.h"
#include "components/UITheme.h"
@@ -43,6 +44,7 @@ void SettingsActivity::onEnter() {
}
// Append device-only ACTION items
displaySettings.push_back(SettingInfo::Action(StrId::STR_SET_TIME, SettingAction::SetTime));
controlsSettings.insert(controlsSettings.begin(),
SettingInfo::Action(StrId::STR_REMAP_FRONT_BUTTONS, SettingAction::RemapFrontButtons));
systemSettings.push_back(SettingInfo::Action(StrId::STR_WIFI_NETWORKS, SettingAction::Network));
@@ -202,6 +204,9 @@ void SettingsActivity::toggleCurrentSetting() {
case SettingAction::Language:
enterSubActivity(new LanguageSelectActivity(renderer, mappedInput, onComplete));
break;
case SettingAction::SetTime:
enterSubActivity(new SetTimeActivity(renderer, mappedInput, onComplete));
break;
case SettingAction::None:
// Do nothing
break;

View File

@@ -21,6 +21,7 @@ enum class SettingAction {
ClearCache,
CheckForUpdates,
Language,
SetTime,
};
struct SettingInfo {

View File

@@ -2,9 +2,12 @@
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <I18n.h>
#include <Utf8.h>
#include <cstdint>
#include <string>
#include <vector>
#include "Battery.h"
#include "RecentBooksStore.h"
@@ -300,69 +303,214 @@ 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 bookCount = std::min(static_cast<int>(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount);
const int tileHeight = rect.height;
const int bookTitleHeight = tileHeight - LyraMetrics::values.homeCoverHeight - hPaddingInSelection;
const int tileY = rect.y;
const bool hasContinueReading = !recentBooks.empty();
const int coverHeight = LyraMetrics::values.homeCoverHeight;
// 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()), 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);
if (bookCount == 0) {
const int centerY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2;
renderer.drawCenteredText(UI_12_FONT_ID, centerY, tr(STR_CHOOSE_SOMETHING), true);
return;
}
// 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();
}
// Word-wrap helper: splits text into lines fitting maxWidth, capped at maxLines with ellipsis
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;
};
// Cover rendering helper: draws bitmap maintaining aspect ratio within a slot.
// Crops if wider than slot, centers if narrower. Returns actual rendered width.
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) {
// ===== SINGLE BOOK: HORIZONTAL LAYOUT (cover left, text right) =====
const bool bookSelected = (selectorIndex == 0);
const int cardX = LyraMetrics::values.contentSidePadding;
const int cardWidth = rect.width - 2 * LyraMetrics::values.contentSidePadding;
// Fixed cover slot width based on typical book aspect ratio (~0.65)
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;
}
for (int i = 0; i < std::min(static_cast<int>(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount); i++) {
bool bookSelected = (selectorIndex == i);
// Selection highlight: border strips around the cover, fill the text area
if (bookSelected) {
// Top strip
renderer.fillRoundedRect(cardX, tileY, cardWidth, hPaddingInSelection, cornerRadius, true, true, false, false,
Color::LightGray);
// Left strip (alongside cover)
renderer.fillRectDither(cardX, tileY + hPaddingInSelection, hPaddingInSelection, coverHeight, Color::LightGray);
// Right strip
renderer.fillRectDither(cardX + cardWidth - hPaddingInSelection, tileY + hPaddingInSelection, hPaddingInSelection,
coverHeight, Color::LightGray);
// Text area background (right of cover, alongside cover height)
renderer.fillRectDither(cardX + hPaddingInSelection + coverSlotWidth, tileY + hPaddingInSelection,
cardWidth - hPaddingInSelection * 2 - coverSlotWidth, coverHeight, Color::LightGray);
// Bottom strip (below cover, full width)
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);
}
}
// Title: UI_12 font, wrap generously (up to 5 lines)
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;
}
// Author: UI_10 font
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 {
// ===== MULTI BOOK: TILE LAYOUT (2-3 books) =====
const int tileWidth = (rect.width - 2 * LyraMetrics::values.contentSidePadding) / bookCount;
// Bottom section height: everything below cover + top padding
const int bottomSectionHeight = tileHeight - coverHeight - hPaddingInSelection;
// Render covers (first render only)
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;
}
// Draw selection and text for each book tile
for (int i = 0; i < bookCount; i++) {
bool bookSelected = (selectorIndex == i);
int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i;
auto title =
renderer.truncatedText(UI_10_FONT_ID, recentBooks[i].title.c_str(), tileWidth - 2 * hPaddingInSelection);
const int maxTextWidth = tileWidth - 2 * hPaddingInSelection;
if (bookSelected) {
// Draw selection box
// Top strip
renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false,
Color::LightGray);
renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection,
LyraMetrics::values.homeCoverHeight, Color::LightGray);
// Left/right strips alongside cover
renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection, coverHeight,
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);
hPaddingInSelection, coverHeight, Color::LightGray);
// Bottom section: spans from below cover to the card bottom
renderer.fillRoundedRect(tileX, tileY + coverHeight + hPaddingInSelection, tileWidth, bottomSectionHeight,
cornerRadius, false, false, true, true, Color::LightGray);
}
// Word-wrap title to 2 lines (UI_10)
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;
}
// Author below title
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);
}
renderer.drawText(UI_10_FONT_ID, tileX + hPaddingInSelection,
tileY + tileHeight - bookTitleHeight + hPaddingInSelection + 5, title.c_str(), true);
}
}
}

View File

@@ -23,7 +23,7 @@ constexpr ThemeMetrics values = {.batteryWidth = 16,
.scrollBarRightOffset = 5,
.homeTopPadding = 56,
.homeCoverHeight = 226,
.homeCoverTileHeight = 287,
.homeCoverTileHeight = 310,
.homeRecentBooksCount = 3,
.buttonHintsHeight = 40,
.sideButtonHintsWidth = 30,