feat: port upstream PRs #852, #965, #972, #971, #977, #975

Port 6 upstream PRs (PR #939 was already ported):

- #852: Complete HalPowerManager with RAII Lock class, WiFi check in
  setPowerSaving, skipLoopDelay overrides for ClearCache/OtaUpdate,
  and power lock in Activity render task loops
- #965: Fix paragraph formatting inside list items by tracking
  listItemUntilDepth to prevent unwanted line breaks
- #972: Micro-optimizations: std::move in insertFont, const ref for
  getDataFromBook parameter
- #971: Remove redundant hasPrintableChars pre-rendering pass from
  EpdFont, EpdFontFamily, and GfxRenderer
- #977: Skip unsupported image formats before extraction, add
  PARSE_BUFFER_SIZE constant and chapter parse timing
- #975: Fix UITheme memory leak by replacing raw pointer with
  std::unique_ptr for currentTheme

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
cottongin
2026-02-18 15:45:06 -05:00
parent 7819cf0f77
commit a1ac11ab51
17 changed files with 100 additions and 45 deletions

View File

@@ -47,14 +47,6 @@ void EpdFont::getTextDimensions(const char* string, int* w, int* h) const {
*h = maxY - minY; *h = maxY - minY;
} }
bool EpdFont::hasPrintableChars(const char* string) const {
int w = 0, h = 0;
getTextDimensions(string, &w, &h);
return w > 0 || h > 0;
}
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const { const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
const EpdUnicodeInterval* intervals = data->intervals; const EpdUnicodeInterval* intervals = data->intervals;
const int count = data->intervalCount; const int count = data->intervalCount;

View File

@@ -9,7 +9,6 @@ class EpdFont {
explicit EpdFont(const EpdFontData* data) : data(data) {} explicit EpdFont(const EpdFontData* data) : data(data) {}
~EpdFont() = default; ~EpdFont() = default;
void getTextDimensions(const char* string, int* w, int* h) const; void getTextDimensions(const char* string, int* w, int* h) const;
bool hasPrintableChars(const char* string) const;
const EpdGlyph* getGlyph(uint32_t cp) const; const EpdGlyph* getGlyph(uint32_t cp) const;
}; };

View File

@@ -22,10 +22,6 @@ void EpdFontFamily::getTextDimensions(const char* string, int* w, int* h, const
getFont(style)->getTextDimensions(string, w, h); getFont(style)->getTextDimensions(string, w, h);
} }
bool EpdFontFamily::hasPrintableChars(const char* string, const Style style) const {
return getFont(style)->hasPrintableChars(string);
}
const EpdFontData* EpdFontFamily::getData(const Style style) const { return getFont(style)->data; } const EpdFontData* EpdFontFamily::getData(const Style style) const { return getFont(style)->data; }
const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const Style style) const { const EpdGlyph* EpdFontFamily::getGlyph(const uint32_t cp, const Style style) const {

View File

@@ -10,7 +10,6 @@ class EpdFontFamily {
: regular(regular), bold(bold), italic(italic), boldItalic(boldItalic) {} : regular(regular), bold(bold), italic(italic), boldItalic(boldItalic) {}
~EpdFontFamily() = default; ~EpdFontFamily() = default;
void getTextDimensions(const char* string, int* w, int* h, Style style = REGULAR) const; void getTextDimensions(const char* string, int* w, int* h, Style style = REGULAR) const;
bool hasPrintableChars(const char* string, Style style = REGULAR) const;
const EpdFontData* getData(Style style = REGULAR) const; const EpdFontData* getData(Style style = REGULAR) const;
const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const; const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const;

View File

@@ -19,6 +19,7 @@ constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
// Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it // Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it
constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB
constexpr size_t PARSE_BUFFER_SIZE = 1024;
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"}; const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]); constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
@@ -389,6 +390,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
// Resolve the image path relative to the HTML file // Resolve the image path relative to the HTML file
std::string resolvedPath = FsHelpers::normalisePath(self->contentBase + src); std::string resolvedPath = FsHelpers::normalisePath(self->contentBase + src);
// Check format support before any file I/O
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(resolvedPath);
if (decoder) {
// Create a unique filename for the cached image // Create a unique filename for the cached image
std::string ext; std::string ext;
size_t extPos = resolvedPath.rfind('.'); size_t extPos = resolvedPath.rfind('.');
@@ -410,8 +414,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
if (extractSuccess) { if (extractSuccess) {
// Get image dimensions // Get image dimensions
ImageDimensions dims = {0, 0}; ImageDimensions dims = {0, 0};
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(cachedImagePath); if (decoder->getDimensions(cachedImagePath, dims)) {
if (decoder && decoder->getDimensions(cachedImagePath, dims)) {
LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height); LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height);
// Scale to fit viewport while maintaining aspect ratio // Scale to fit viewport while maintaining aspect ratio
@@ -470,6 +473,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
} else { } else {
LOG_ERR("EHP", "Failed to extract image"); LOG_ERR("EHP", "Failed to extract image");
} }
} // if (decoder)
} }
} }
@@ -540,18 +544,24 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) { } else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
if (strcmp(name, "br") == 0) { if (strcmp(name, "br") == 0) {
if (self->partWordBufferIndex > 0) { if (self->partWordBufferIndex > 0) {
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
self->flushPartWordBuffer(); self->flushPartWordBuffer();
} }
self->startNewTextBlock(self->currentTextBlock->getBlockStyle()); self->startNewTextBlock(self->currentTextBlock->getBlockStyle());
} else if (strcmp(name, "li") == 0) {
self->currentCssStyle = cssStyle;
self->startNewTextBlock(userAlignmentBlockStyle);
self->updateEffectiveInlineStyle();
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
self->listItemUntilDepth = std::min(self->listItemUntilDepth, self->depth);
} else if (strcmp(name, "p") == 0 && self->listItemUntilDepth < self->depth) {
// Inside a <li> element - don't start a new text block for <p>
// This prevents bullet points from appearing on their own line
self->currentCssStyle = cssStyle;
self->updateEffectiveInlineStyle();
} else { } else {
self->currentCssStyle = cssStyle; self->currentCssStyle = cssStyle;
self->startNewTextBlock(userAlignmentBlockStyle); self->startNewTextBlock(userAlignmentBlockStyle);
self->updateEffectiveInlineStyle(); self->updateEffectiveInlineStyle();
if (strcmp(name, "li") == 0) {
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
}
} }
} else if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) { } else if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) {
// Flush buffer before style change so preceding text gets current style // Flush buffer before style change so preceding text gets current style
@@ -807,6 +817,7 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
if (self->boldUntilDepth == self->depth) self->boldUntilDepth = INT_MAX; if (self->boldUntilDepth == self->depth) self->boldUntilDepth = INT_MAX;
if (self->italicUntilDepth == self->depth) self->italicUntilDepth = INT_MAX; if (self->italicUntilDepth == self->depth) self->italicUntilDepth = INT_MAX;
if (self->underlineUntilDepth == self->depth) self->underlineUntilDepth = INT_MAX; if (self->underlineUntilDepth == self->depth) self->underlineUntilDepth = INT_MAX;
if (self->listItemUntilDepth == self->depth) self->listItemUntilDepth = INT_MAX;
if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) { if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) {
self->inlineStyleStack.pop_back(); self->inlineStyleStack.pop_back();
self->updateEffectiveInlineStyle(); self->updateEffectiveInlineStyle();
@@ -852,6 +863,11 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
self->underlineUntilDepth = INT_MAX; self->underlineUntilDepth = INT_MAX;
} }
// Leaving list item
if (self->listItemUntilDepth == self->depth) {
self->listItemUntilDepth = INT_MAX;
}
// Pop from inline style stack if we pushed an entry at this depth // Pop from inline style stack if we pushed an entry at this depth
// This handles all inline elements: b, i, u, span, etc. // This handles all inline elements: b, i, u, span, etc.
if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) { if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) {
@@ -867,6 +883,7 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
} }
bool ChapterHtmlSlimParser::parseAndBuildPages() { bool ChapterHtmlSlimParser::parseAndBuildPages() {
unsigned long chapterStartTime = millis();
auto paragraphAlignmentBlockStyle = BlockStyle(); auto paragraphAlignmentBlockStyle = BlockStyle();
paragraphAlignmentBlockStyle.textAlignDefined = true; paragraphAlignmentBlockStyle.textAlignDefined = true;
// Resolve None sentinel to Justify for initial block (no CSS context yet) // Resolve None sentinel to Justify for initial block (no CSS context yet)
@@ -904,7 +921,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
XML_SetCharacterDataHandler(parser, characterData); XML_SetCharacterDataHandler(parser, characterData);
do { do {
void* const buf = XML_GetBuffer(parser, 1024); void* const buf = XML_GetBuffer(parser, PARSE_BUFFER_SIZE);
if (!buf) { if (!buf) {
LOG_ERR("EHP", "Couldn't allocate memory for buffer"); LOG_ERR("EHP", "Couldn't allocate memory for buffer");
XML_StopParser(parser, XML_FALSE); // Stop any pending processing XML_StopParser(parser, XML_FALSE); // Stop any pending processing
@@ -915,7 +932,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
return false; return false;
} }
const size_t len = file.read(buf, 1024); const size_t len = file.read(buf, PARSE_BUFFER_SIZE);
if (len == 0 && file.available() > 0) { if (len == 0 && file.available() > 0) {
LOG_ERR("EHP", "File read error"); LOG_ERR("EHP", "File read error");
@@ -955,6 +972,7 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() {
currentTextBlock.reset(); currentTextBlock.reset();
} }
LOG_DBG("EHP", "Chapter parsed in %lu ms", millis() - chapterStartTime);
return true; return true;
} }

View File

@@ -31,6 +31,7 @@ class ChapterHtmlSlimParser {
int boldUntilDepth = INT_MAX; int boldUntilDepth = INT_MAX;
int italicUntilDepth = INT_MAX; int italicUntilDepth = INT_MAX;
int underlineUntilDepth = INT_MAX; int underlineUntilDepth = INT_MAX;
int listItemUntilDepth = INT_MAX;
// buffer for building up words from characters, will auto break if longer than this // buffer for building up words from characters, will auto break if longer than this
// leave one char at end for null pointer // leave one char at end for null pointer
char partWordBuffer[MAX_WORD_SIZE + 1] = {}; char partWordBuffer[MAX_WORD_SIZE + 1] = {};

View File

@@ -11,7 +11,7 @@ void GfxRenderer::begin() {
} }
} }
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); } void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, std::move(font)}); }
// Translate logical (x,y) coordinates to physical panel coordinates based on current orientation // Translate logical (x,y) coordinates to physical panel coordinates based on current orientation
// This should always be inlined for better performance // This should always be inlined for better performance
@@ -116,11 +116,6 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
} }
const auto font = fontMap.at(fontId); const auto font = fontMap.at(fontId);
// no printable characters
if (!font.hasPrintableChars(text, style)) {
return;
}
uint32_t cp; uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) { while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
renderChar(font, cp, &xpos, &yPos, black, style); renderChar(font, cp, &xpos, &yPos, black, style);
@@ -853,11 +848,6 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
} }
const auto font = fontMap.at(fontId); const auto font = fontMap.at(fontId);
// No printable characters
if (!font.hasPrintableChars(text, style)) {
return;
}
// For 90° clockwise rotation: // For 90° clockwise rotation:
// Original (glyphX, glyphY) -> Rotated (glyphY, -glyphX) // Original (glyphX, glyphY) -> Rotated (glyphY, -glyphX)
// Text reads from bottom to top // Text reads from bottom to top
@@ -936,11 +926,6 @@ void GfxRenderer::drawTextRotated90CCW(const int fontId, const int x, const int
} }
const auto font = fontMap.at(fontId); const auto font = fontMap.at(fontId);
// No printable characters
if (!font.hasPrintableChars(text, style)) {
return;
}
// For 90° counter-clockwise rotation: // For 90° counter-clockwise rotation:
// Mirror of CW: glyphY maps to -X direction, glyphX maps to +Y direction // Mirror of CW: glyphY maps to -X direction, glyphX maps to +Y direction
// Text reads from top to bottom // Text reads from top to bottom

View File

@@ -1,6 +1,7 @@
#include "HalPowerManager.h" #include "HalPowerManager.h"
#include <Logging.h> #include <Logging.h>
#include <WiFi.h>
#include <esp_sleep.h> #include <esp_sleep.h>
#include "HalGPIO.h" #include "HalGPIO.h"
@@ -8,12 +9,27 @@
void HalPowerManager::begin() { void HalPowerManager::begin() {
pinMode(BAT_GPIO0, INPUT); pinMode(BAT_GPIO0, INPUT);
normalFreq = getCpuFrequencyMhz(); normalFreq = getCpuFrequencyMhz();
modeMutex = xSemaphoreCreateMutex();
assert(modeMutex != nullptr);
} }
void HalPowerManager::setPowerSaving(bool enabled) { void HalPowerManager::setPowerSaving(bool enabled) {
if (normalFreq <= 0) { if (normalFreq <= 0) {
return; // invalid state return;
} }
if (enabled) {
if (WiFi.getMode() != WIFI_MODE_NULL) {
enabled = false;
}
xSemaphoreTake(modeMutex, portMAX_DELAY);
const LockMode mode = currentLockMode;
xSemaphoreGive(modeMutex);
if (mode == NormalSpeed) {
enabled = false;
}
}
if (enabled && !isLowPower) { if (enabled && !isLowPower) {
LOG_DBG("PWR", "Going to low-power mode"); LOG_DBG("PWR", "Going to low-power mode");
if (!setCpuFrequencyMhz(LOW_POWER_FREQ)) { if (!setCpuFrequencyMhz(LOW_POWER_FREQ)) {
@@ -31,6 +47,25 @@ void HalPowerManager::setPowerSaving(bool enabled) {
isLowPower = enabled; isLowPower = enabled;
} }
// RAII Lock implementation
HalPowerManager::Lock::Lock() {
xSemaphoreTake(powerManager.modeMutex, portMAX_DELAY);
powerManager.currentLockMode = NormalSpeed;
valid = true;
if (powerManager.isLowPower) {
powerManager.setPowerSaving(false);
}
xSemaphoreGive(powerManager.modeMutex);
}
HalPowerManager::Lock::~Lock() {
if (!valid) return;
xSemaphoreTake(powerManager.modeMutex, portMAX_DELAY);
powerManager.currentLockMode = None;
xSemaphoreGive(powerManager.modeMutex);
}
void HalPowerManager::startDeepSleep(HalGPIO& gpio) const { void HalPowerManager::startDeepSleep(HalGPIO& gpio) const {
// Ensure that the power button has been released to avoid immediately turning back on if you're holding it // Ensure that the power button has been released to avoid immediately turning back on if you're holding it
while (gpio.isPressed(HalGPIO::BTN_POWER)) { while (gpio.isPressed(HalGPIO::BTN_POWER)) {

View File

@@ -3,6 +3,7 @@
#include <Arduino.h> #include <Arduino.h>
#include <BatteryMonitor.h> #include <BatteryMonitor.h>
#include <InputManager.h> #include <InputManager.h>
#include <freertos/semphr.h>
#include "HalGPIO.h" #include "HalGPIO.h"
@@ -10,6 +11,10 @@ class HalPowerManager {
int normalFreq = 0; // MHz int normalFreq = 0; // MHz
bool isLowPower = false; bool isLowPower = false;
enum LockMode { None, NormalSpeed };
LockMode currentLockMode = None;
SemaphoreHandle_t modeMutex = nullptr;
public: public:
static constexpr int LOW_POWER_FREQ = 10; // MHz static constexpr int LOW_POWER_FREQ = 10; // MHz
static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // ms static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // ms
@@ -24,4 +29,20 @@ class HalPowerManager {
// Get battery percentage (range 0-100) // Get battery percentage (range 0-100)
int getBatteryPercentage() const; int getBatteryPercentage() const;
// RAII lock to prevent low-power mode during critical work (e.g. rendering)
class Lock {
friend class HalPowerManager;
bool valid = false;
public:
Lock();
~Lock();
Lock(const Lock&) = delete;
Lock& operator=(const Lock&) = delete;
Lock(Lock&&) = delete;
Lock& operator=(Lock&&) = delete;
};
}; };
extern HalPowerManager powerManager;

View File

@@ -85,7 +85,7 @@ bool RecentBooksStore::saveToFile() const {
return true; return true;
} }
RecentBook RecentBooksStore::getDataFromBook(std::string path) const { RecentBook RecentBooksStore::getDataFromBook(const std::string& path) const {
std::string lastBookFileName = ""; std::string lastBookFileName = "";
const size_t lastSlash = path.find_last_of('/'); const size_t lastSlash = path.find_last_of('/');
if (lastSlash != std::string::npos) { if (lastSlash != std::string::npos) {

View File

@@ -42,7 +42,7 @@ class RecentBooksStore {
bool saveToFile() const; bool saveToFile() const;
bool loadFromFile(); bool loadFromFile();
RecentBook getDataFromBook(std::string path) const; RecentBook getDataFromBook(const std::string& path) const;
}; };
// Helper macro to access recent books store // Helper macro to access recent books store

View File

@@ -1,5 +1,7 @@
#include "Activity.h" #include "Activity.h"
#include <HalPowerManager.h>
void Activity::renderTaskTrampoline(void* param) { void Activity::renderTaskTrampoline(void* param) {
auto* self = static_cast<Activity*>(param); auto* self = static_cast<Activity*>(param);
self->renderTaskLoop(); self->renderTaskLoop();
@@ -9,6 +11,7 @@ void Activity::renderTaskLoop() {
while (true) { while (true) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
{ {
HalPowerManager::Lock powerLock;
RenderLock lock(*this); RenderLock lock(*this);
render(std::move(lock)); render(std::move(lock));
} }

View File

@@ -1,9 +1,12 @@
#include "ActivityWithSubactivity.h" #include "ActivityWithSubactivity.h"
#include <HalPowerManager.h>
void ActivityWithSubactivity::renderTaskLoop() { void ActivityWithSubactivity::renderTaskLoop() {
while (true) { while (true) {
ulTaskNotifyTake(pdTRUE, portMAX_DELAY); ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
{ {
HalPowerManager::Lock powerLock;
RenderLock lock(*this); RenderLock lock(*this);
if (!subActivity) { if (!subActivity) {
render(std::move(lock)); render(std::move(lock));

View File

@@ -14,6 +14,7 @@ class ClearCacheActivity final : public ActivityWithSubactivity {
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override; void render(Activity::RenderLock&&) override;
bool skipLoopDelay() override { return true; }
private: private:
enum State { WARNING, CLEARING, SUCCESS, FAILED }; enum State { WARNING, CLEARING, SUCCESS, FAILED };

View File

@@ -33,5 +33,6 @@ class OtaUpdateActivity : public ActivityWithSubactivity {
void onExit() override; void onExit() override;
void loop() override; void loop() override;
void render(Activity::RenderLock&&) override; void render(Activity::RenderLock&&) override;
bool skipLoopDelay() override { return true; }
bool preventAutoSleep() override { return state == CHECKING_FOR_UPDATE || state == UPDATE_IN_PROGRESS; } bool preventAutoSleep() override { return state == CHECKING_FOR_UPDATE || state == UPDATE_IN_PROGRESS; }
}; };

View File

@@ -25,12 +25,12 @@ void UITheme::setTheme(CrossPointSettings::UI_THEME type) {
switch (type) { switch (type) {
case CrossPointSettings::UI_THEME::CLASSIC: case CrossPointSettings::UI_THEME::CLASSIC:
LOG_DBG("UI", "Using Classic theme"); LOG_DBG("UI", "Using Classic theme");
currentTheme = new BaseTheme(); currentTheme = std::make_unique<BaseTheme>();
currentMetrics = &BaseMetrics::values; currentMetrics = &BaseMetrics::values;
break; break;
case CrossPointSettings::UI_THEME::LYRA: case CrossPointSettings::UI_THEME::LYRA:
LOG_DBG("UI", "Using Lyra theme"); LOG_DBG("UI", "Using Lyra theme");
currentTheme = new LyraTheme(); currentTheme = std::make_unique<LyraTheme>();
currentMetrics = &LyraMetrics::values; currentMetrics = &LyraMetrics::values;
break; break;
} }

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <functional> #include <functional>
#include <memory>
#include <vector> #include <vector>
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
@@ -24,7 +25,7 @@ class UITheme {
private: private:
const ThemeMetrics* currentMetrics; const ThemeMetrics* currentMetrics;
const BaseTheme* currentTheme; std::unique_ptr<const BaseTheme> currentTheme;
}; };
// Known theme thumbnail heights to prerender when opening a book for the first time. // Known theme thumbnail heights to prerender when opening a book for the first time.