fix: Port upstream 1.1.0-rc PRs #1014, #1018, #990 and align #1002

Port three new upstream commits and align the existing #1002 port:

- PR #1014: Strip unused CSS rules by filtering unsupported selector
  types (+, >, [, :, #, ~, *, descendants) in processRuleBlockWithStyle.
  Fix normalized() trailing whitespace to also strip newlines.
- PR #1018: Add deleteCache() to CssParser, move CSS_CACHE_VERSION to
  static class member, remove stale cache on version mismatch, invalidate
  section caches (Storage.removeDir) when CSS is rebuilt. Refactor
  parseCssFiles() to early-return when cache exists.
- PR #990: Adapt classic theme continue-reading card width to cover
  aspect ratio (clamped to 90% screen width), increase homeTopPadding
  20->40, fix centering with rect.x offset for boxX/continueBoxX.
- #1002 alignment: Add tryInterpretLength() to skip non-numeric CSS
  values (auto, inherit), add "both width and height set" image sizing
  branch in ChapterHtmlSlimParser.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
cottongin
2026-02-20 15:52:30 -05:00
parent 18be265a4a
commit 55a1fef01a
6 changed files with 195 additions and 109 deletions

View File

@@ -213,23 +213,23 @@ bool Epub::parseTocNavFile() const {
} }
void Epub::parseCssFiles() const { void Epub::parseCssFiles() const {
// Maximum CSS file size we'll attempt to parse (uncompressed) constexpr size_t MAX_CSS_FILE_SIZE = 128 * 1024;
// Larger files risk memory exhaustion on ESP32 constexpr size_t MIN_HEAP_FOR_CSS_PARSING = 64 * 1024;
constexpr size_t MAX_CSS_FILE_SIZE = 128 * 1024; // 128KB
// Minimum heap required before attempting CSS parsing
constexpr size_t MIN_HEAP_FOR_CSS_PARSING = 64 * 1024; // 64KB
if (cssFiles.empty()) { if (cssFiles.empty()) {
LOG_DBG("EBP", "No CSS files to parse, but CssParser created for inline styles"); LOG_DBG("EBP", "No CSS files to parse, but CssParser created for inline styles");
} }
// See if we have a cached version of the CSS rules LOG_DBG("EBP", "CSS files to parse: %zu", cssFiles.size());
if (!cssParser->hasCache()) {
// No cache yet - parse CSS files if (cssParser->hasCache()) {
LOG_DBG("EBP", "CSS cache exists, skipping parseCssFiles");
return;
}
for (const auto& cssPath : cssFiles) { for (const auto& cssPath : cssFiles) {
LOG_DBG("EBP", "Parsing CSS file: %s", cssPath.c_str()); LOG_DBG("EBP", "Parsing CSS file: %s", cssPath.c_str());
// Check heap before parsing - CSS parsing allocates heavily
const uint32_t freeHeap = ESP.getFreeHeap(); const uint32_t freeHeap = ESP.getFreeHeap();
if (freeHeap < MIN_HEAP_FOR_CSS_PARSING) { if (freeHeap < MIN_HEAP_FOR_CSS_PARSING) {
LOG_ERR("EBP", "Insufficient heap for CSS parsing (%u bytes free, need %zu), skipping: %s", freeHeap, LOG_ERR("EBP", "Insufficient heap for CSS parsing (%u bytes free, need %zu), skipping: %s", freeHeap,
@@ -237,7 +237,6 @@ void Epub::parseCssFiles() const {
continue; continue;
} }
// Check CSS file size before decompressing - skip files that are too large
size_t cssFileSize = 0; size_t cssFileSize = 0;
if (getItemSize(cssPath, &cssFileSize)) { if (getItemSize(cssPath, &cssFileSize)) {
if (cssFileSize > MAX_CSS_FILE_SIZE) { if (cssFileSize > MAX_CSS_FILE_SIZE) {
@@ -247,7 +246,6 @@ void Epub::parseCssFiles() const {
} }
} }
// Extract CSS file to temp location
const auto tmpCssPath = getCachePath() + "/.tmp.css"; const auto tmpCssPath = getCachePath() + "/.tmp.css";
FsFile tempCssFile; FsFile tempCssFile;
if (!Storage.openFileForWrite("EBP", tmpCssPath, tempCssFile)) { if (!Storage.openFileForWrite("EBP", tmpCssPath, tempCssFile)) {
@@ -262,7 +260,6 @@ void Epub::parseCssFiles() const {
} }
tempCssFile.close(); tempCssFile.close();
// Parse the CSS file
if (!Storage.openFileForRead("EBP", tmpCssPath, tempCssFile)) { if (!Storage.openFileForRead("EBP", tmpCssPath, tempCssFile)) {
LOG_ERR("EBP", "Could not open temp CSS file for reading"); LOG_ERR("EBP", "Could not open temp CSS file for reading");
Storage.remove(tmpCssPath.c_str()); Storage.remove(tmpCssPath.c_str());
@@ -273,7 +270,6 @@ void Epub::parseCssFiles() const {
Storage.remove(tmpCssPath.c_str()); Storage.remove(tmpCssPath.c_str());
} }
// Save to cache for next time
if (!cssParser->saveToCache()) { if (!cssParser->saveToCache()) {
LOG_ERR("EBP", "Failed to save CSS rules to cache"); LOG_ERR("EBP", "Failed to save CSS rules to cache");
} }
@@ -281,7 +277,6 @@ void Epub::parseCssFiles() const {
LOG_DBG("EBP", "Loaded %zu CSS style rules from %zu files", cssParser->ruleCount(), cssFiles.size()); LOG_DBG("EBP", "Loaded %zu CSS style rules from %zu files", cssParser->ruleCount(), cssFiles.size());
} }
}
// load in the meta data for the epub file // load in the meta data for the epub file
bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) { bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
@@ -294,14 +289,17 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
// Try to load existing cache first // Try to load existing cache first
if (bookMetadataCache->load()) { if (bookMetadataCache->load()) {
if (!skipLoadingCss && !cssParser->hasCache()) { if (!skipLoadingCss) {
LOG_DBG("EBP", "Warning: CSS rules cache not found, attempting to parse CSS files"); if (!cssParser->hasCache() || !cssParser->loadFromCache()) {
// to get CSS file list LOG_DBG("EBP", "CSS rules cache missing or stale, attempting to parse CSS files");
cssParser->deleteCache();
if (!parseContentOpf(bookMetadataCache->coreMetadata)) { if (!parseContentOpf(bookMetadataCache->coreMetadata)) {
LOG_ERR("EBP", "Could not parse content.opf from cached bookMetadata for CSS files"); LOG_ERR("EBP", "Could not parse content.opf from cached bookMetadata for CSS files");
// continue anyway - book will work without CSS and we'll still load any inline style CSS
} }
parseCssFiles(); parseCssFiles();
Storage.removeDir((cachePath + "/sections").c_str());
}
} }
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str()); LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
return true; return true;
@@ -400,8 +398,8 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
} }
if (!skipLoadingCss) { if (!skipLoadingCss) {
// Parse CSS files after cache reload
parseCssFiles(); parseCssFiles();
Storage.removeDir((cachePath + "/sections").c_str());
} }
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str()); LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());

View File

@@ -74,7 +74,7 @@ std::string CssParser::normalized(const std::string& s) {
} }
// Remove trailing space // Remove trailing space
if (!result.empty() && result.back() == ' ') { while (!result.empty() && (result.back() == ' ' || result.back() == '\n')) {
result.pop_back(); result.pop_back();
} }
return result; return result;
@@ -189,10 +189,18 @@ CssTextDecoration CssParser::interpretDecoration(const std::string& val) {
} }
CssLength CssParser::interpretLength(const std::string& val) { CssLength CssParser::interpretLength(const std::string& val) {
const std::string v = normalized(val); CssLength result;
if (v.empty()) return CssLength{}; tryInterpretLength(val, result);
return result;
}
bool CssParser::tryInterpretLength(const std::string& val, CssLength& out) {
const std::string v = normalized(val);
if (v.empty()) {
out = CssLength{};
return false;
}
// Find where the number ends
size_t unitStart = v.size(); size_t unitStart = v.size();
for (size_t i = 0; i < v.size(); ++i) { for (size_t i = 0; i < v.size(); ++i) {
const char c = v[i]; const char c = v[i];
@@ -205,12 +213,13 @@ CssLength CssParser::interpretLength(const std::string& val) {
const std::string numPart = v.substr(0, unitStart); const std::string numPart = v.substr(0, unitStart);
const std::string unitPart = v.substr(unitStart); const std::string unitPart = v.substr(unitStart);
// Parse numeric value
char* endPtr = nullptr; char* endPtr = nullptr;
const float numericValue = std::strtof(numPart.c_str(), &endPtr); const float numericValue = std::strtof(numPart.c_str(), &endPtr);
if (endPtr == numPart.c_str()) return CssLength{}; // No number parsed if (endPtr == numPart.c_str()) {
out = CssLength{};
return false; // No number parsed (e.g. auto, inherit, initial)
}
// Determine unit type (preserve for deferred resolution)
auto unit = CssUnit::Pixels; auto unit = CssUnit::Pixels;
if (unitPart == "em") { if (unitPart == "em") {
unit = CssUnit::Em; unit = CssUnit::Em;
@@ -221,9 +230,9 @@ CssLength CssParser::interpretLength(const std::string& val) {
} else if (unitPart == "%") { } else if (unitPart == "%") {
unit = CssUnit::Percent; unit = CssUnit::Percent;
} }
// px and unitless default to Pixels
return CssLength{numericValue, unit}; out = CssLength{numericValue, unit};
return true;
} }
// Declaration parsing // Declaration parsing
@@ -296,13 +305,19 @@ void CssParser::parseDeclarationIntoStyle(const std::string& decl, CssStyle& sty
1; 1;
} }
} else if (propNameBuf == "height") { } else if (propNameBuf == "height") {
style.imageHeight = interpretLength(propValueBuf); CssLength len;
if (tryInterpretLength(propValueBuf, len)) {
style.imageHeight = len;
style.defined.imageHeight = 1; style.defined.imageHeight = 1;
}
} else if (propNameBuf == "width") { } else if (propNameBuf == "width") {
style.width = interpretLength(propValueBuf); CssLength len;
if (tryInterpretLength(propValueBuf, len)) {
style.width = len;
style.defined.width = 1; style.defined.width = 1;
} }
} }
}
CssStyle CssParser::parseDeclarations(const std::string& declBlock) { CssStyle CssParser::parseDeclarations(const std::string& declBlock) {
CssStyle style; CssStyle style;
@@ -349,6 +364,17 @@ void CssParser::processRuleBlockWithStyle(const std::string& selectorGroup, cons
std::string key = normalized(sel); std::string key = normalized(sel);
if (key.empty()) continue; if (key.empty()) continue;
// Skip unsupported selector types to reduce memory usage.
// We only match: tag, tag.class, .class
if (key.find('+') != std::string::npos) continue; // adjacent sibling
if (key.find('>') != std::string::npos) continue; // child combinator
if (key.find('[') != std::string::npos) continue; // attribute selector
if (key.find(':') != std::string::npos) continue; // pseudo selector
if (key.find('#') != std::string::npos) continue; // ID selector
if (key.find('~') != std::string::npos) continue; // general sibling
if (key.find('*') != std::string::npos) continue; // wildcard
if (key.find(' ') != std::string::npos) continue; // descendant combinator
// Skip if this would exceed the rule limit // Skip if this would exceed the rule limit
if (rulesBySelector_.size() >= MAX_RULES) { if (rulesBySelector_.size() >= MAX_RULES) {
LOG_DBG("CSS", "Reached max rules limit, stopping selector processing"); LOG_DBG("CSS", "Reached max rules limit, stopping selector processing");
@@ -534,6 +560,7 @@ CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string&
result.applyOver(tagIt->second); result.applyOver(tagIt->second);
} }
// TODO: Support combinations of classes (e.g. style on .class1.class2)
// 2. Apply class styles (medium priority) // 2. Apply class styles (medium priority)
if (!classAttr.empty()) { if (!classAttr.empty()) {
const auto classes = splitWhitespace(classAttr); const auto classes = splitWhitespace(classAttr);
@@ -547,6 +574,7 @@ CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string&
} }
} }
// TODO: Support combinations of classes (e.g. style on p.class1.class2)
// 3. Apply element.class styles (higher priority) // 3. Apply element.class styles (higher priority)
for (const auto& cls : classes) { for (const auto& cls : classes) {
std::string combinedKey = tag + "." + normalized(cls); std::string combinedKey = tag + "." + normalized(cls);
@@ -567,12 +595,15 @@ CssStyle CssParser::parseInlineStyle(const std::string& styleValue) { return par
// Cache serialization // Cache serialization
// Cache format version - increment when format changes // Cache file name (version is CssParser::CSS_CACHE_VERSION)
constexpr uint8_t CSS_CACHE_VERSION = 3;
constexpr char rulesCache[] = "/css_rules.cache"; constexpr char rulesCache[] = "/css_rules.cache";
bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); } bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); }
void CssParser::deleteCache() const {
if (hasCache()) Storage.remove((cachePath + rulesCache).c_str());
}
bool CssParser::saveToCache() const { bool CssParser::saveToCache() const {
if (cachePath.empty()) { if (cachePath.empty()) {
return false; return false;
@@ -584,7 +615,7 @@ bool CssParser::saveToCache() const {
} }
// Write version // Write version
file.write(CSS_CACHE_VERSION); file.write(CssParser::CSS_CACHE_VERSION);
// Write rule count // Write rule count
const auto ruleCount = static_cast<uint16_t>(rulesBySelector_.size()); const auto ruleCount = static_cast<uint16_t>(rulesBySelector_.size());
@@ -662,9 +693,11 @@ bool CssParser::loadFromCache() {
// Read and verify version // Read and verify version
uint8_t version = 0; uint8_t version = 0;
if (file.read(&version, 1) != 1 || version != CSS_CACHE_VERSION) { if (file.read(&version, 1) != 1 || version != CssParser::CSS_CACHE_VERSION) {
LOG_DBG("CSS", "Cache version mismatch (got %u, expected %u)", version, CSS_CACHE_VERSION); LOG_DBG("CSS", "Cache version mismatch (got %u, expected %u), removing stale cache for rebuild", version,
CssParser::CSS_CACHE_VERSION);
file.close(); file.close();
Storage.remove((cachePath + rulesCache).c_str());
return false; return false;
} }

View File

@@ -82,6 +82,11 @@ class CssParser {
*/ */
bool hasCache() const; bool hasCache() const;
/**
* Delete CSS rules cache file if it exists
*/
void deleteCache() const;
/** /**
* Save parsed CSS rules to a cache file. * Save parsed CSS rules to a cache file.
* @return true if cache was written successfully * @return true if cache was written successfully
@@ -91,10 +96,14 @@ class CssParser {
/** /**
* Load CSS rules from a cache file. * Load CSS rules from a cache file.
* Clears any existing rules before loading. * Clears any existing rules before loading.
* Removes stale cache file on version mismatch.
* @return true if cache was loaded successfully * @return true if cache was loaded successfully
*/ */
bool loadFromCache(); bool loadFromCache();
// Bump when CSS cache format or rules change; section caches are invalidated when this changes
static constexpr uint8_t CSS_CACHE_VERSION = 3;
private: private:
// Storage: maps normalized selector -> style properties // Storage: maps normalized selector -> style properties
std::unordered_map<std::string, CssStyle> rulesBySelector_; std::unordered_map<std::string, CssStyle> rulesBySelector_;
@@ -113,6 +122,7 @@ class CssParser {
static CssFontWeight interpretFontWeight(const std::string& val); static CssFontWeight interpretFontWeight(const std::string& val);
static CssTextDecoration interpretDecoration(const std::string& val); static CssTextDecoration interpretDecoration(const std::string& val);
static CssLength interpretLength(const std::string& val); static CssLength interpretLength(const std::string& val);
static bool tryInterpretLength(const std::string& val, CssLength& out);
// String utilities // String utilities
static std::string normalized(const std::string& s); static std::string normalized(const std::string& s);

View File

@@ -429,12 +429,39 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
const bool hasCssHeight = imgStyle.hasImageHeight(); const bool hasCssHeight = imgStyle.hasImageHeight();
const bool hasCssWidth = imgStyle.hasWidth(); const bool hasCssWidth = imgStyle.hasWidth();
if (hasCssHeight && dims.width > 0 && dims.height > 0) { if (hasCssHeight && hasCssWidth && dims.width > 0 && dims.height > 0) {
displayHeight = static_cast<int>(
imgStyle.imageHeight.toPixels(emSize, static_cast<float>(self->viewportHeight)) + 0.5f);
displayWidth = static_cast<int>(
imgStyle.width.toPixels(emSize, static_cast<float>(self->viewportWidth)) + 0.5f);
if (displayHeight < 1) displayHeight = 1;
if (displayWidth < 1) displayWidth = 1;
if (displayWidth > self->viewportWidth || displayHeight > self->viewportHeight) {
float scaleX = (displayWidth > self->viewportWidth)
? static_cast<float>(self->viewportWidth) / displayWidth
: 1.0f;
float scaleY = (displayHeight > self->viewportHeight)
? static_cast<float>(self->viewportHeight) / displayHeight
: 1.0f;
float scale = (scaleX < scaleY) ? scaleX : scaleY;
displayWidth = static_cast<int>(displayWidth * scale + 0.5f);
displayHeight = static_cast<int>(displayHeight * scale + 0.5f);
if (displayWidth < 1) displayWidth = 1;
if (displayHeight < 1) displayHeight = 1;
}
LOG_DBG("EHP", "Display size from CSS height+width: %dx%d", displayWidth, displayHeight);
} else if (hasCssHeight && !hasCssWidth && dims.width > 0 && dims.height > 0) {
displayHeight = static_cast<int>( displayHeight = static_cast<int>(
imgStyle.imageHeight.toPixels(emSize, static_cast<float>(self->viewportHeight)) + 0.5f); imgStyle.imageHeight.toPixels(emSize, static_cast<float>(self->viewportHeight)) + 0.5f);
if (displayHeight < 1) displayHeight = 1; if (displayHeight < 1) displayHeight = 1;
displayWidth = displayWidth =
static_cast<int>(displayHeight * (static_cast<float>(dims.width) / dims.height) + 0.5f); static_cast<int>(displayHeight * (static_cast<float>(dims.width) / dims.height) + 0.5f);
if (displayHeight > self->viewportHeight) {
displayHeight = self->viewportHeight;
displayWidth =
static_cast<int>(displayHeight * (static_cast<float>(dims.width) / dims.height) + 0.5f);
if (displayWidth < 1) displayWidth = 1;
}
if (displayWidth > self->viewportWidth) { if (displayWidth > self->viewportWidth) {
displayWidth = self->viewportWidth; displayWidth = self->viewportWidth;
displayHeight = displayHeight =

View File

@@ -365,14 +365,52 @@ void BaseTheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const s
void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks, void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
const int selectorIndex, bool& coverRendered, bool& coverBufferStored, const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
bool& bufferRestored, std::function<bool()> storeCoverBuffer) const { bool& bufferRestored, std::function<bool()> storeCoverBuffer) const {
// --- Top "book" card for the current title (selectorIndex == 0) ---
const int bookWidth = rect.width / 2;
const int bookHeight = rect.height;
const int bookX = (rect.width - bookWidth) / 2;
const int bookY = rect.y;
const bool hasContinueReading = !recentBooks.empty(); const bool hasContinueReading = !recentBooks.empty();
const bool bookSelected = hasContinueReading && selectorIndex == 0; const bool bookSelected = hasContinueReading && selectorIndex == 0;
// --- Top "book" card for the current title (selectorIndex == 0) ---
// Adapt width to cover image aspect ratio; fall back to half screen when no cover
const int baseHeight = rect.height;
int bookWidth;
bool hasCoverImage = false;
if (hasContinueReading && !recentBooks[0].coverBmpPath.empty()) {
const std::string coverBmpPath =
UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, BaseMetrics::values.homeCoverHeight);
FsFile file;
if (Storage.openFileForRead("HOME", coverBmpPath, file)) {
Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
hasCoverImage = true;
const int imgWidth = bitmap.getWidth();
const int imgHeight = bitmap.getHeight();
if (imgWidth > 0 && imgHeight > 0) {
const float aspectRatio = static_cast<float>(imgWidth) / static_cast<float>(imgHeight);
bookWidth = static_cast<int>(baseHeight * aspectRatio);
const int maxWidth = static_cast<int>(rect.width * 0.9f);
if (bookWidth > maxWidth) {
bookWidth = maxWidth;
}
} else {
bookWidth = rect.width / 2;
}
}
file.close();
}
}
if (!hasCoverImage) {
bookWidth = rect.width / 2;
}
const int bookX = rect.x + (rect.width - bookWidth) / 2;
const int bookY = rect.y;
const int bookHeight = baseHeight;
// Bookmark dimensions (used in multiple places) // Bookmark dimensions (used in multiple places)
const int bookmarkWidth = bookWidth / 8; const int bookmarkWidth = bookWidth / 8;
const int bookmarkHeight = bookHeight / 5; const int bookmarkHeight = bookHeight / 5;
@@ -394,29 +432,9 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
Bitmap bitmap(file); Bitmap bitmap(file);
if (bitmap.parseHeaders() == BmpReaderError::Ok) { if (bitmap.parseHeaders() == BmpReaderError::Ok) {
LOG_DBG("THEME", "Rendering bmp"); LOG_DBG("THEME", "Rendering bmp");
// Calculate position to center image within the book card
int coverX, coverY;
if (bitmap.getWidth() > bookWidth || bitmap.getHeight() > bookHeight) { renderer.drawBitmap(bitmap, bookX, bookY, bookWidth, bookHeight);
const float imgRatio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
const float boxRatio = static_cast<float>(bookWidth) / static_cast<float>(bookHeight);
if (imgRatio > boxRatio) {
coverX = bookX;
coverY = bookY + (bookHeight - static_cast<int>(bookWidth / imgRatio)) / 2;
} else {
coverX = bookX + (bookWidth - static_cast<int>(bookHeight * imgRatio)) / 2;
coverY = bookY;
}
} else {
coverX = bookX + (bookWidth - bitmap.getWidth()) / 2;
coverY = bookY + (bookHeight - bitmap.getHeight()) / 2;
}
// Draw the cover image centered within the book card
renderer.drawBitmap(bitmap, coverX, coverY, bookWidth, bookHeight);
// Draw border around the card
renderer.drawRect(bookX, bookY, bookWidth, bookHeight); renderer.drawRect(bookX, bookY, bookWidth, bookHeight);
// No bookmark ribbon when cover is shown - it would just cover the art // No bookmark ribbon when cover is shown - it would just cover the art
@@ -597,7 +615,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
const int boxWidth = maxTextWidth + boxPadding * 2; const int boxWidth = maxTextWidth + boxPadding * 2;
const int boxHeight = totalTextHeight + boxPadding * 2; const int boxHeight = totalTextHeight + boxPadding * 2;
const int boxX = (rect.width - boxWidth) / 2; const int boxX = rect.x + (rect.width - boxWidth) / 2;
const int boxY = titleYStart - boxPadding; const int boxY = titleYStart - boxPadding;
// Draw box (inverted when selected: black box instead of white) // Draw box (inverted when selected: black box instead of white)
@@ -640,7 +658,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
constexpr int continuePadding = 6; constexpr int continuePadding = 6;
const int continueBoxWidth = continueTextWidth + continuePadding * 2; const int continueBoxWidth = continueTextWidth + continuePadding * 2;
const int continueBoxHeight = renderer.getLineHeight(UI_10_FONT_ID) + continuePadding; const int continueBoxHeight = renderer.getLineHeight(UI_10_FONT_ID) + continuePadding;
const int continueBoxX = (rect.width - continueBoxWidth) / 2; const int continueBoxX = rect.x + (rect.width - continueBoxWidth) / 2;
const int continueBoxY = continueY - continuePadding / 2; const int continueBoxY = continueY - continuePadding / 2;
renderer.fillRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, bookSelected); renderer.fillRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, bookSelected);
renderer.drawRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, !bookSelected); renderer.drawRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, !bookSelected);

View File

@@ -82,7 +82,7 @@ constexpr ThemeMetrics values = {.batteryWidth = 15,
.tabBarHeight = 50, .tabBarHeight = 50,
.scrollBarWidth = 4, .scrollBarWidth = 4,
.scrollBarRightOffset = 5, .scrollBarRightOffset = 5,
.homeTopPadding = 20, .homeTopPadding = 40,
.homeCoverHeight = 400, .homeCoverHeight = 400,
.homeCoverTileHeight = 400, .homeCoverTileHeight = 400,
.homeRecentBooksCount = 1, .homeRecentBooksCount = 1,