From 41d333c76b14ed711bfdfda4f06f9043baad04b2 Mon Sep 17 00:00:00 2001 From: CaptainFrito Date: Tue, 10 Feb 2026 16:43:11 +0700 Subject: [PATCH 01/11] feat: Lyra theme - File transfer menu --- .../network/NetworkModeSelectionActivity.cpp | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/src/activities/network/NetworkModeSelectionActivity.cpp b/src/activities/network/NetworkModeSelectionActivity.cpp index bee13d8c1..985994ee9 100644 --- a/src/activities/network/NetworkModeSelectionActivity.cpp +++ b/src/activities/network/NetworkModeSelectionActivity.cpp @@ -33,7 +33,7 @@ void NetworkModeSelectionActivity::onEnter() { updateRequired = true; xTaskCreate(&NetworkModeSelectionActivity::taskTrampoline, "NetworkModeTask", - 2048, // Stack size + 4096, // Stack size this, // Parameters 1, // Priority &displayTaskHandle // Task handle @@ -97,38 +97,26 @@ void NetworkModeSelectionActivity::displayTaskLoop() { } void NetworkModeSelectionActivity::render() const { - renderer.clearScreen(); - + auto metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); - // Draw header - renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer", true, EpdFontFamily::BOLD); - - // Draw subtitle - renderer.drawCenteredText(UI_10_FONT_ID, 50, "How would you like to connect?"); - - // Draw menu items centered on screen - constexpr int itemHeight = 50; // Height for each menu item (including description) - const int startY = (pageHeight - (MENU_ITEM_COUNT * itemHeight)) / 2 + 10; + renderer.clearScreen(); - for (int i = 0; i < MENU_ITEM_COUNT; i++) { - const int itemY = startY + i * itemHeight; - const bool isSelected = (i == selectedIndex); + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "File Transfer"); - // Draw selection highlight (black fill) for selected item - if (isSelected) { - renderer.fillRect(20, itemY - 2, pageWidth - 40, itemHeight - 6); - } + const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; + GUI.drawList( + renderer, + Rect{0, contentTop, pageWidth, contentHeight}, + static_cast(MENU_ITEM_COUNT), selectedIndex, + [this](int index) { return MENU_ITEMS[index]; }, + [this](int index) { return MENU_DESCRIPTIONS[index]; }, nullptr, nullptr); - // Draw text: black=false (white text) when selected (on black background) - // black=true (black text) when not selected (on white background) - renderer.drawText(UI_10_FONT_ID, 30, itemY, MENU_ITEMS[i], /*black=*/!isSelected); - renderer.drawText(SMALL_FONT_ID, 30, itemY + 22, MENU_DESCRIPTIONS[i], /*black=*/!isSelected); - } // Draw help text at bottom - const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); + const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); From b8889800cee4ba970c628c547c7c04b42ca6080e Mon Sep 17 00:00:00 2001 From: CaptainFrito Date: Tue, 10 Feb 2026 16:44:43 +0700 Subject: [PATCH 02/11] feat: Lyra theme - Wifi selection screen --- src/activities/home/MyLibraryActivity.cpp | 1 - .../network/WifiSelectionActivity.cpp | 114 ++++++------------ .../network/WifiSelectionActivity.h | 2 +- src/activities/settings/SettingsActivity.cpp | 7 +- src/components/UITheme.cpp | 31 +++++ src/components/UITheme.h | 5 +- src/components/themes/BaseTheme.cpp | 32 ++++- src/components/themes/BaseTheme.h | 6 +- src/components/themes/lyra/LyraTheme.cpp | 28 ++++- src/components/themes/lyra/LyraTheme.h | 4 +- 10 files changed, 135 insertions(+), 95 deletions(-) diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 3d5dbf1a8..c2c97d941 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -191,7 +191,6 @@ void MyLibraryActivity::loop() { } int listSize = static_cast(files.size()); - buttonNavigator.onNextRelease([this, listSize] { selectorIndex = ButtonNavigator::nextIndex(static_cast(selectorIndex), listSize); updateRequired = true; diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 83af4e07f..ce777a39f 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -178,13 +178,12 @@ void WifiSelectionActivity::processWifiScanResults() { networks.push_back(pair.second); } - // Sort by signal strength (strongest first) - std::sort(networks.begin(), networks.end(), - [](const WifiNetworkInfo& a, const WifiNetworkInfo& b) { return a.rssi > b.rssi; }); - - // Show networks with PW first + // Sort: saved-password networks first, then by signal strength (strongest first) std::sort(networks.begin(), networks.end(), [](const WifiNetworkInfo& a, const WifiNetworkInfo& b) { - return a.hasSavedPassword && !b.hasSavedPassword; + if (a.hasSavedPassword != b.hasSavedPassword) { + return a.hasSavedPassword && !b.hasSavedPassword; + } + return a.rssi > b.rssi; }); WiFi.scanDelete(); @@ -490,15 +489,12 @@ std::string WifiSelectionActivity::getSignalStrengthIndicator(const int32_t rssi return "||||"; // Excellent } if (rssi >= -60) { - return "||| "; // Good + return " |||"; // Good } if (rssi >= -70) { - return "|| "; // Fair + return " ||"; // Fair } - if (rssi >= -80) { - return "| "; // Weak - } - return " "; // Very weak + return " |"; // Very weak } void WifiSelectionActivity::displayTaskLoop() { @@ -529,6 +525,16 @@ void WifiSelectionActivity::displayTaskLoop() { void WifiSelectionActivity::render() const { renderer.clearScreen(); + auto metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + + // Draw header + char countStr[32]; + snprintf(countStr, sizeof(countStr), "%zu found", networks.size()); + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "WiFi Networks", countStr); + GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, cachedMacAddress.c_str()); + switch (state) { case WifiSelectionState::AUTO_CONNECTING: renderConnecting(); @@ -560,12 +566,10 @@ void WifiSelectionActivity::render() const { } void WifiSelectionActivity::renderNetworkList() const { + auto metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); - // Draw header - renderer.drawCenteredText(UI_12_FONT_ID, 15, "WiFi Networks", true, EpdFontFamily::BOLD); - if (networks.empty()) { // No networks found or scan failed const auto height = renderer.getLineHeight(UI_10_FONT_ID); @@ -573,69 +577,23 @@ void WifiSelectionActivity::renderNetworkList() const { renderer.drawCenteredText(UI_10_FONT_ID, top, "No networks found"); renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press Connect to scan again"); } else { - // Calculate how many networks we can display - constexpr int startY = 60; - constexpr int lineHeight = 25; - const int maxVisibleNetworks = (pageHeight - startY - 40) / lineHeight; - - // Calculate scroll offset to keep selected item visible - int scrollOffset = 0; - if (selectedNetworkIndex >= maxVisibleNetworks) { - scrollOffset = selectedNetworkIndex - maxVisibleNetworks + 1; - } - - // Draw networks - int displayIndex = 0; - for (size_t i = scrollOffset; i < networks.size() && displayIndex < maxVisibleNetworks; i++, displayIndex++) { - const int networkY = startY + displayIndex * lineHeight; - const auto& network = networks[i]; - - // Draw selection indicator - if (static_cast(i) == selectedNetworkIndex) { - renderer.drawText(UI_10_FONT_ID, 5, networkY, ">"); - } - - // Draw network name (truncate if too long) - std::string displayName = network.ssid; - if (displayName.length() > 33) { - displayName.replace(30, displayName.length() - 30, "..."); - } - renderer.drawText(UI_10_FONT_ID, 20, networkY, displayName.c_str()); - - // Draw signal strength indicator - std::string signalStr = getSignalStrengthIndicator(network.rssi); - renderer.drawText(UI_10_FONT_ID, pageWidth - 90, networkY, signalStr.c_str()); - - // Draw saved indicator (checkmark) for networks with saved passwords - if (network.hasSavedPassword) { - renderer.drawText(UI_10_FONT_ID, pageWidth - 50, networkY, "+"); - } - - // Draw lock icon for encrypted networks - if (network.isEncrypted) { - renderer.drawText(UI_10_FONT_ID, pageWidth - 30, networkY, "*"); - } - } - - // Draw scroll indicators if needed - if (scrollOffset > 0) { - renderer.drawText(SMALL_FONT_ID, pageWidth - 15, startY - 10, "^"); - } - if (scrollOffset + maxVisibleNetworks < static_cast(networks.size())) { - renderer.drawText(SMALL_FONT_ID, pageWidth - 15, startY + maxVisibleNetworks * lineHeight, "v"); - } - - // Show network count - char countStr[32]; - snprintf(countStr, sizeof(countStr), "%zu networks found", networks.size()); - renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 90, countStr); - } - - // Show MAC address above the network count and legend - renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 105, cachedMacAddress.c_str()); - - // Draw help text - renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved"); + int contentTop = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing; + int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; + GUI.drawList( + renderer, Rect{0, contentTop, pageWidth, contentHeight}, + static_cast(networks.size()), selectedNetworkIndex, + [this](int index) { return networks[index].ssid; }, nullptr, nullptr, + [this](int index) { + auto network = networks[index]; + return std::string(network.hasSavedPassword ? "+ " : "") + + (network.isEncrypted ? "* " : "") + + getSignalStrengthIndicator(network.rssi); + }); + } + + GUI.drawHelpText(renderer, + Rect{0, pageHeight - metrics.buttonHintsHeight - 26, pageWidth, 20}, + "* = Encrypted ||| = Strength + = Saved"); const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword; const char* forgetLabel = hasSavedPassword ? "Forget" : ""; diff --git a/src/activities/network/WifiSelectionActivity.h b/src/activities/network/WifiSelectionActivity.h index 32eb36dbd..9790e349a 100644 --- a/src/activities/network/WifiSelectionActivity.h +++ b/src/activities/network/WifiSelectionActivity.h @@ -50,7 +50,7 @@ class WifiSelectionActivity final : public ActivityWithSubactivity { ButtonNavigator buttonNavigator; bool updateRequired = false; WifiSelectionState state = WifiSelectionState::SCANNING; - int selectedNetworkIndex = 0; + size_t selectedNetworkIndex = 0; std::vector networks; const std::function onComplete; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 7d3a60169..21338f9e1 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -248,7 +248,7 @@ void SettingsActivity::render() const { auto metrics = UITheme::getInstance().getMetrics(); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "Settings"); + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "Settings", CROSSPOINT_VERSION); std::vector tabs; tabs.reserve(categoryCount); @@ -280,11 +280,6 @@ void SettingsActivity::render() const { return valueText; }); - // Draw version text - renderer.drawText(SMALL_FONT_ID, - pageWidth - metrics.versionTextRightX - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION), - metrics.versionTextY, CROSSPOINT_VERSION); - // Draw help text const auto labels = mappedInput.mapLabels("« Back", "Toggle", "Up", "Down"); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 9dbe429e3..53b6e0606 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -7,6 +7,11 @@ #include "RecentBooksStore.h" #include "components/themes/BaseTheme.h" #include "components/themes/lyra/LyraTheme.h" +#include "MappedInputManager.h" + +namespace { +constexpr int SKIP_PAGE_MS = 700; +} // namespace UITheme UITheme::instance; @@ -60,3 +65,29 @@ std::string UITheme::getCoverThumbPath(std::string coverBmpPath, int coverHeight } return coverBmpPath; } + +void UITheme::handleListScrolling(const GfxRenderer& renderer, int listSize, int pageItems, size_t& selectorIndex, + const MappedInputManager& mappedInput, bool& updateRequired) { + const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left) || + mappedInput.wasReleased(MappedInputManager::Button::Up); + const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Right) || + mappedInput.wasReleased(MappedInputManager::Button::Down); + + const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; + + if (upReleased) { + if (skipPage) { + selectorIndex = std::max(static_cast((selectorIndex / pageItems - 1) * pageItems), 0); + } else { + selectorIndex = (selectorIndex + listSize - 1) % listSize; + } + updateRequired = true; + } else if (downReleased) { + if (skipPage) { + selectorIndex = std::min(static_cast((selectorIndex / pageItems + 1) * pageItems), listSize - 1); + } else { + selectorIndex = (selectorIndex + 1) % listSize; + } + updateRequired = true; + } +} diff --git a/src/components/UITheme.h b/src/components/UITheme.h index 0a30223ba..102ebc682 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -6,6 +6,8 @@ #include "CrossPointSettings.h" #include "components/themes/BaseTheme.h" +class MappedInputManager; + class UITheme { // Static instance static UITheme instance; @@ -21,7 +23,8 @@ class UITheme { static int getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints, bool hasSubtitle); static std::string getCoverThumbPath(std::string coverBmpPath, int coverHeight); - + void handleListScrolling(const GfxRenderer& renderer, int listSize, int pageItems, size_t& selectorIndex, + const MappedInputManager& mappedInput, bool& updateRequired); private: const ThemeMetrics* currentMetrics; const BaseTheme* currentTheme; diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 14783e558..d4099db0a 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -17,6 +17,7 @@ namespace { constexpr int batteryPercentSpacing = 4; constexpr int homeMenuMargin = 20; constexpr int homeMarginTop = 30; +constexpr int subtitleY = 738; } // namespace void BaseTheme::drawBattery(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { @@ -227,7 +228,7 @@ 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 { const bool showBatteryPercentage = SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS; int batteryX = rect.x + rect.width - BaseMetrics::values.contentSidePadding - BaseMetrics::values.batteryWidth; @@ -246,6 +247,28 @@ 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 { + constexpr int underlineHeight = 2; // Height of selection underline + constexpr int underlineGap = 4; // Gap between text and underline + + const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); + + int currentX = rect.x + BaseMetrics::values.contentSidePadding; + + auto truncatedLabel = renderer.truncatedText( + UI_12_FONT_ID, label, rect.width - BaseMetrics::values.contentSidePadding * 2, 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& tabs, @@ -646,3 +669,10 @@ 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()); +} \ No newline at end of file diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index 038a2913d..37ada094c 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -83,7 +83,6 @@ constexpr ThemeMetrics values = {.batteryWidth = 15, .buttonHintsHeight = 40, .sideButtonHintsWidth = 30, .versionTextRightX = 20, - .versionTextY = 738, .bookProgressBarHeight = 4}; } @@ -102,8 +101,8 @@ class BaseTheme { const std::function& rowSubtitle, const std::function& rowIcon, const std::function& rowValue) const; - - virtual void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) 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; virtual void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector& tabs, bool selected) const; virtual void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector& recentBooks, @@ -115,4 +114,5 @@ 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; }; \ No newline at end of file diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 2e3ad4cd8..44cc77f35 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -18,6 +18,7 @@ constexpr int batteryPercentSpacing = 4; constexpr int hPaddingInSelection = 8; constexpr int cornerRadius = 6; constexpr int topHintButtonY = 345; +constexpr int maxSubtitleWidth = 100; } // namespace void LyraTheme::drawBattery(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { @@ -57,7 +58,7 @@ void LyraTheme::drawBattery(const GfxRenderer& renderer, Rect rect, const bool s } } -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 = @@ -72,14 +73,37 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t Rect{batteryX, rect.y + 10, 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); + 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 { + int currentX = rect.x + LyraMetrics::values.contentSidePadding; + + auto truncatedLabel = renderer.truncatedText( + UI_10_FONT_ID, label, rect.width - LyraMetrics::values.contentSidePadding * 2, 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& tabs, diff --git a/src/components/themes/lyra/LyraTheme.h b/src/components/themes/lyra/LyraTheme.h index 93ec05792..41ea670ec 100644 --- a/src/components/themes/lyra/LyraTheme.h +++ b/src/components/themes/lyra/LyraTheme.h @@ -28,7 +28,6 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, .buttonHintsHeight = 40, .sideButtonHintsWidth = 30, .versionTextRightX = 20, - .versionTextY = 55, .bookProgressBarHeight = 4}; } @@ -37,7 +36,8 @@ class LyraTheme : public BaseTheme { // Component drawing methods // void drawProgressBar(const GfxRenderer& renderer, Rect rect, size_t current, size_t total) override; void drawBattery(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 override; void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector& tabs, bool selected) const override; void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, From 0d3e61278046e45d8dd89f5b659409427c416ace Mon Sep 17 00:00:00 2001 From: CaptainFrito Date: Tue, 10 Feb 2026 16:44:43 +0700 Subject: [PATCH 03/11] feat: Lyra theme - Hotspot and web server screens --- .../network/CrossPointWebServerActivity.cpp | 88 ++++++++++--------- .../network/NetworkModeSelectionActivity.cpp | 9 +- .../network/WifiSelectionActivity.cpp | 32 ++++--- src/activities/settings/SettingsActivity.cpp | 3 +- src/components/UITheme.cpp | 4 +- src/components/UITheme.h | 3 +- src/components/themes/BaseTheme.cpp | 14 ++- src/components/themes/BaseTheme.h | 3 +- src/components/themes/lyra/LyraTheme.cpp | 23 +++-- 9 files changed, 88 insertions(+), 91 deletions(-) diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index 0338d8257..db16ca24f 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -23,6 +23,8 @@ constexpr const char* AP_PASSWORD = nullptr; // Open network for ease of use constexpr const char* AP_HOSTNAME = "crosspoint"; constexpr uint8_t AP_CHANNEL = 1; constexpr uint8_t AP_MAX_CONNECTIONS = 4; +constexpr int QR_CODE_WIDTH = 6 * 33; +constexpr int QR_CODE_HEIGHT = 200; // DNS server for captive portal (redirects all DNS queries to our IP) DNSServer* dnsServer = nullptr; @@ -51,7 +53,7 @@ void CrossPointWebServerActivity::onEnter() { updateRequired = true; xTaskCreate(&CrossPointWebServerActivity::taskTrampoline, "WebServerActivityTask", - 2048, // Stack size + 4096, // Stack size this, // Parameters 1, // Priority &displayTaskHandle // Task handle @@ -416,67 +418,69 @@ void drawQRCode(const GfxRenderer& renderer, const int x, const int y, const std } void CrossPointWebServerActivity::renderServerRunning() const { - // Use consistent line spacing - constexpr int LINE_SPACING = 28; // Space between lines + auto metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer", true, EpdFontFamily::BOLD); + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, + isApMode ? "Hotspot Mode" : "Network Transfer", nullptr); + GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, + connectedSSID.c_str()); + int startY = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 2; if (isApMode) { - // AP mode display - center the content block - int startY = 55; + // AP mode display + renderer.drawText(UI_12_FONT_ID, metrics.contentSidePadding, startY, "Scan QR code or connect to Wifi", true, + EpdFontFamily::BOLD); + startY += renderer.getLineHeight(UI_12_FONT_ID) + metrics.verticalSpacing * 2; - renderer.drawCenteredText(UI_10_FONT_ID, startY, "Hotspot Mode", true, EpdFontFamily::BOLD); + // Show QR code for Wifi + const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;"; + drawQRCode(renderer, metrics.contentSidePadding, startY, wifiConfig); - std::string ssidInfo = "Network: " + connectedSSID; - renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ssidInfo.c_str()); + // Show network name + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 80, + connectedSSID.c_str()); + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 100, + "network"); - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 2, "Connect your device to this WiFi network"); + startY += QR_CODE_HEIGHT + 2 * metrics.verticalSpacing; - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, - "or scan QR code with your phone to connect to Wifi."); - // Show QR code for URL - const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;"; - drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 4, wifiConfig); - - startY += 6 * 29 + 3 * LINE_SPACING; // Show primary URL (hostname) - std::string hostnameUrl = std::string("http://") + AP_HOSTNAME + ".local/"; - renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str(), true, EpdFontFamily::BOLD); + renderer.drawText(UI_12_FONT_ID, metrics.contentSidePadding, startY, "Scan QR code or open URL", true, + EpdFontFamily::BOLD); + startY += renderer.getLineHeight(UI_12_FONT_ID) + metrics.verticalSpacing * 2; - // Show IP address as fallback + std::string hostnameUrl = std::string("http://") + AP_HOSTNAME + ".local/"; std::string ipUrl = "or http://" + connectedIP + "/"; - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, ipUrl.c_str()); - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "Open this URL in your browser"); // Show QR code for URL - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, "or scan QR code with your phone:"); - drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 7, hostnameUrl); + drawQRCode(renderer, metrics.contentSidePadding, startY, hostnameUrl); + + // Show IP address as fallback + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 80, + hostnameUrl.c_str()); + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 100, + ipUrl.c_str()); } else { - // STA mode display (original behavior) - const int startY = 65; + startY += metrics.verticalSpacing * 2; - std::string ssidInfo = "Network: " + connectedSSID; - if (ssidInfo.length() > 28) { - ssidInfo.replace(25, ssidInfo.length() - 25, "..."); - } - renderer.drawCenteredText(UI_10_FONT_ID, startY, ssidInfo.c_str()); + // STA mode display (original behavior) + // std::string ipInfo = "IP Address: " + connectedIP; + renderer.drawCenteredText(UI_12_FONT_ID, startY, "Scan QR code or open URL", true, EpdFontFamily::BOLD); + startY += renderer.getLineHeight(UI_12_FONT_ID) + metrics.verticalSpacing * 2; - std::string ipInfo = "IP Address: " + connectedIP; - renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ipInfo.c_str()); + // Show QR code for URL + std::string webInfo = "http://" + connectedIP + "/"; + drawQRCode(renderer, (pageWidth - QR_CODE_WIDTH) / 2, startY, webInfo); + startY += QR_CODE_HEIGHT + metrics.verticalSpacing * 2; // Show web server URL prominently - std::string webInfo = "http://" + connectedIP + "/"; - renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 2, webInfo.c_str(), true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, startY, webInfo.c_str(), true); + startY += renderer.getLineHeight(UI_10_FONT_ID) + 5; // Also show hostname URL std::string hostnameUrl = std::string("or http://") + AP_HOSTNAME + ".local/"; - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str()); - - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, "Open this URL in your browser"); - - // Show QR code for URL - drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 6, webInfo); - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "or scan QR code with your phone:"); + renderer.drawCenteredText(SMALL_FONT_ID, startY, hostnameUrl.c_str(), true); } const auto labels = mappedInput.mapLabels("« Exit", "", "", ""); diff --git a/src/activities/network/NetworkModeSelectionActivity.cpp b/src/activities/network/NetworkModeSelectionActivity.cpp index 985994ee9..e320e44f4 100644 --- a/src/activities/network/NetworkModeSelectionActivity.cpp +++ b/src/activities/network/NetworkModeSelectionActivity.cpp @@ -108,12 +108,9 @@ void NetworkModeSelectionActivity::render() const { const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; GUI.drawList( - renderer, - Rect{0, contentTop, pageWidth, contentHeight}, - static_cast(MENU_ITEM_COUNT), selectedIndex, - [this](int index) { return MENU_ITEMS[index]; }, - [this](int index) { return MENU_DESCRIPTIONS[index]; }, nullptr, nullptr); - + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(MENU_ITEM_COUNT), selectedIndex, + [this](int index) { return MENU_ITEMS[index]; }, [this](int index) { return MENU_DESCRIPTIONS[index]; }, nullptr, + nullptr); // Draw help text at bottom const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index ce777a39f..988ecfc46 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -181,7 +181,7 @@ void WifiSelectionActivity::processWifiScanResults() { // Sort: saved-password networks first, then by signal strength (strongest first) std::sort(networks.begin(), networks.end(), [](const WifiNetworkInfo& a, const WifiNetworkInfo& b) { if (a.hasSavedPassword != b.hasSavedPassword) { - return a.hasSavedPassword && !b.hasSavedPassword; + return a.hasSavedPassword; } return a.rssi > b.rssi; }); @@ -531,9 +531,10 @@ void WifiSelectionActivity::render() const { // Draw header char countStr[32]; - snprintf(countStr, sizeof(countStr), "%zu found", networks.size()); + snprintf(countStr, sizeof(countStr), "%zu found", networks.size()); GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "WiFi Networks", countStr); - GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, cachedMacAddress.c_str()); + GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, + cachedMacAddress.c_str()); switch (state) { case WifiSelectionState::AUTO_CONNECTING: @@ -580,20 +581,17 @@ void WifiSelectionActivity::renderNetworkList() const { int contentTop = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing; int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; GUI.drawList( - renderer, Rect{0, contentTop, pageWidth, contentHeight}, - static_cast(networks.size()), selectedNetworkIndex, - [this](int index) { return networks[index].ssid; }, nullptr, nullptr, - [this](int index) { - auto network = networks[index]; - return std::string(network.hasSavedPassword ? "+ " : "") - + (network.isEncrypted ? "* " : "") - + getSignalStrengthIndicator(network.rssi); - }); - } - - GUI.drawHelpText(renderer, - Rect{0, pageHeight - metrics.buttonHintsHeight - 26, pageWidth, 20}, - "* = Encrypted ||| = Strength + = Saved"); + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(networks.size()), + selectedNetworkIndex, [this](int index) { return networks[index].ssid; }, nullptr, nullptr, + [this](int index) { + auto network = networks[index]; + return std::string(network.hasSavedPassword ? "+ " : "") + (network.isEncrypted ? "* " : "") + + getSignalStrengthIndicator(network.rssi); + }); + } + + GUI.drawHelpText(renderer, Rect{0, pageHeight - metrics.buttonHintsHeight - 26, pageWidth, 20}, + "* = Encrypted ||| = Strength + = Saved"); const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword; const char* forgetLabel = hasSavedPassword ? "Forget" : ""; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 21338f9e1..b1bdbb752 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -248,7 +248,8 @@ void SettingsActivity::render() const { auto metrics = UITheme::getInstance().getMetrics(); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "Settings", CROSSPOINT_VERSION); + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "Settings", + CROSSPOINT_VERSION); std::vector tabs; tabs.reserve(categoryCount); diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 53b6e0606..6be567f5c 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -4,10 +4,10 @@ #include +#include "MappedInputManager.h" #include "RecentBooksStore.h" #include "components/themes/BaseTheme.h" #include "components/themes/lyra/LyraTheme.h" -#include "MappedInputManager.h" namespace { constexpr int SKIP_PAGE_MS = 700; @@ -66,7 +66,7 @@ std::string UITheme::getCoverThumbPath(std::string coverBmpPath, int coverHeight return coverBmpPath; } -void UITheme::handleListScrolling(const GfxRenderer& renderer, int listSize, int pageItems, size_t& selectorIndex, +void UITheme::handleListScrolling(const GfxRenderer& renderer, int listSize, int pageItems, size_t& selectorIndex, const MappedInputManager& mappedInput, bool& updateRequired) { const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left) || mappedInput.wasReleased(MappedInputManager::Button::Up); diff --git a/src/components/UITheme.h b/src/components/UITheme.h index 102ebc682..aa4c66a5a 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -24,7 +24,8 @@ class UITheme { bool hasSubtitle); static std::string getCoverThumbPath(std::string coverBmpPath, int coverHeight); void handleListScrolling(const GfxRenderer& renderer, int listSize, int pageItems, size_t& selectorIndex, - const MappedInputManager& mappedInput, bool& updateRequired); + const MappedInputManager& mappedInput, bool& updateRequired); + private: const ThemeMetrics* currentMetrics; const BaseTheme* currentTheme; diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index d4099db0a..8b9bd183f 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -252,9 +252,9 @@ void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t 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); + renderer.drawText(SMALL_FONT_ID, + rect.x + rect.width - BaseMetrics::values.contentSidePadding - truncatedSubtitleWidth, subtitleY, + truncatedSubtitle.c_str(), true); } } @@ -262,12 +262,10 @@ void BaseTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char constexpr int underlineHeight = 2; // Height of selection underline constexpr int underlineGap = 4; // Gap between text and underline - const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); - int currentX = rect.x + BaseMetrics::values.contentSidePadding; auto truncatedLabel = renderer.truncatedText( - UI_12_FONT_ID, label, rect.width - BaseMetrics::values.contentSidePadding * 2, EpdFontFamily::REGULAR); + UI_12_FONT_ID, label, rect.width - BaseMetrics::values.contentSidePadding * 2, EpdFontFamily::REGULAR); renderer.drawText(UI_12_FONT_ID, currentX, rect.y, truncatedLabel.c_str(), true, EpdFontFamily::REGULAR); } @@ -672,7 +670,7 @@ void BaseTheme::drawReadingProgressBar(const GfxRenderer& renderer, const size_t 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); + 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()); } \ No newline at end of file diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index 37ada094c..ad8888444 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -101,7 +101,8 @@ class BaseTheme { const std::function& rowSubtitle, const std::function& rowIcon, const std::function& rowValue) const; - virtual void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle = nullptr) 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; virtual void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector& tabs, bool selected) const; diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 44cc77f35..642a54996 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -73,12 +73,11 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t Rect{batteryX, rect.y + 10, LyraMetrics::values.batteryWidth, LyraMetrics::values.batteryHeight}, showBatteryPercentage); - int maxTitleWidth = rect.width - LyraMetrics::values.contentSidePadding * 2 - - (subtitle != nullptr ? maxSubtitleWidth : 0); - + int maxTitleWidth = + rect.width - LyraMetrics::values.contentSidePadding * 2 - (subtitle != nullptr ? maxSubtitleWidth : 0); + if (title) { - auto truncatedTitle = renderer.truncatedText( - UI_12_FONT_ID, title, maxTitleWidth, EpdFontFamily::BOLD); + 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); @@ -86,12 +85,11 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t } if (subtitle) { - auto truncatedSubtitle = renderer.truncatedText( - SMALL_FONT_ID, subtitle, maxSubtitleWidth, EpdFontFamily::REGULAR); + 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); + renderer.drawText(SMALL_FONT_ID, + rect.x + rect.width - LyraMetrics::values.contentSidePadding - truncatedSubtitleWidth, + rect.y + 50, truncatedSubtitle.c_str(), true); } } @@ -100,9 +98,8 @@ void LyraTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char auto truncatedLabel = renderer.truncatedText( UI_10_FONT_ID, label, rect.width - LyraMetrics::values.contentSidePadding * 2, EpdFontFamily::REGULAR); - renderer.drawText(UI_10_FONT_ID, currentX, rect.y + 6, truncatedLabel.c_str(), true, - 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); } From c6232c860d5783a7ca821e79b28ea2ed46fdf46a Mon Sep 17 00:00:00 2001 From: CaptainFrito Date: Tue, 10 Feb 2026 16:44:43 +0700 Subject: [PATCH 04/11] Visual fixes --- .../network/CrossPointWebServerActivity.cpp | 22 ++++++++++++++----- .../network/WifiSelectionActivity.cpp | 3 ++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index db16ca24f..efb6bbe20 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -383,14 +383,24 @@ void CrossPointWebServerActivity::displayTaskLoop() { void CrossPointWebServerActivity::render() const { // Only render our own UI when server is running // Subactivities handle their own rendering - if (state == WebServerActivityState::SERVER_RUNNING) { - renderer.clearScreen(); - renderServerRunning(); - renderer.displayBuffer(); - } else if (state == WebServerActivityState::AP_STARTING) { + if (state == WebServerActivityState::SERVER_RUNNING || state == WebServerActivityState::AP_STARTING) { renderer.clearScreen(); + auto metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); - renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Starting Hotspot...", true, EpdFontFamily::BOLD); + + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, + isApMode ? "Hotspot Mode" : "Network Transfer", nullptr); + + if (state == WebServerActivityState::SERVER_RUNNING) { + GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, + connectedSSID.c_str()); + renderServerRunning(); + } else { + const auto height = renderer.getLineHeight(UI_10_FONT_ID); + const auto top = (pageHeight - height) / 2; + renderer.drawCenteredText(UI_10_FONT_ID, top, "Starting Hotspot..."); + } renderer.displayBuffer(); } } diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 988ecfc46..e2c3a0ee4 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -590,7 +590,8 @@ void WifiSelectionActivity::renderNetworkList() const { }); } - GUI.drawHelpText(renderer, Rect{0, pageHeight - metrics.buttonHintsHeight - 26, pageWidth, 20}, + GUI.drawHelpText(renderer, + Rect{0, pageHeight - metrics.buttonHintsHeight - metrics.contentSidePadding - 15, pageWidth, 20}, "* = Encrypted ||| = Strength + = Saved"); const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword; From 2b0046234fe428fd545b384197cf36a7c600e275 Mon Sep 17 00:00:00 2001 From: CaptainFrito Date: Tue, 10 Feb 2026 16:44:43 +0700 Subject: [PATCH 05/11] feat: Lyra theme - KOReader screens --- .../settings/CalibreSettingsActivity.cpp | 58 ++++++++--------- .../settings/CalibreSettingsActivity.h | 2 +- .../settings/KOReaderAuthActivity.cpp | 41 ++++++------ .../settings/KOReaderSettingsActivity.cpp | 64 +++++++++---------- .../settings/KOReaderSettingsActivity.h | 2 +- src/components/themes/BaseTheme.h | 6 +- src/components/themes/lyra/LyraTheme.cpp | 35 +++++----- 7 files changed, 97 insertions(+), 111 deletions(-) diff --git a/src/activities/settings/CalibreSettingsActivity.cpp b/src/activities/settings/CalibreSettingsActivity.cpp index 7b7a0ed48..e34a0d134 100644 --- a/src/activities/settings/CalibreSettingsActivity.cpp +++ b/src/activities/settings/CalibreSettingsActivity.cpp @@ -150,41 +150,35 @@ void CalibreSettingsActivity::displayTaskLoop() { } void CalibreSettingsActivity::render() { - renderer.clearScreen(); - + auto metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); - // Draw header - renderer.drawCenteredText(UI_12_FONT_ID, 15, "OPDS Browser", true, EpdFontFamily::BOLD); - - // Draw info text about Calibre - renderer.drawCenteredText(UI_10_FONT_ID, 40, "For Calibre, add /opds to your URL"); - - // Draw selection highlight - renderer.fillRect(0, 70 + selectedIndex * 30 - 2, pageWidth - 1, 30); - - // Draw menu items - for (int i = 0; i < MENU_ITEMS; i++) { - const int settingY = 70 + i * 30; - const bool isSelected = (i == selectedIndex); - - renderer.drawText(UI_10_FONT_ID, 20, settingY, menuNames[i], !isSelected); - - // Draw status for each setting - const char* status = "[Not Set]"; - if (i == 0) { - status = (strlen(SETTINGS.opdsServerUrl) > 0) ? "[Set]" : "[Not Set]"; - } else if (i == 1) { - status = (strlen(SETTINGS.opdsUsername) > 0) ? "[Set]" : "[Not Set]"; - } else if (i == 2) { - status = (strlen(SETTINGS.opdsPassword) > 0) ? "[Set]" : "[Not Set]"; - } - const auto width = renderer.getTextWidth(UI_10_FONT_ID, status); - renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected); - } + renderer.clearScreen(); - // Draw button hints - const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "OPDS Browser"); + GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, + "For Calibre, add /opds to your URL"); + + const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + metrics.tabBarHeight; + const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; + GUI.drawList( + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(MENU_ITEMS), + static_cast(selectedIndex), [this](int index) { return menuNames[index]; }, nullptr, nullptr, + [this](int index) { + // Draw status for each setting + if (index == 0) { + return (strlen(SETTINGS.opdsServerUrl) > 0) ? SETTINGS.opdsServerUrl : "[Not Set]"; + } else if (index == 1) { + return (strlen(SETTINGS.opdsUsername) > 0) ? SETTINGS.opdsUsername : "[Not Set]"; + } else if (index == 2) { + return (strlen(SETTINGS.opdsPassword) > 0) ? "******" : "[Not Set]"; + } + return "[Not Set]"; + }); + + // Draw help text at bottom + const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); diff --git a/src/activities/settings/CalibreSettingsActivity.h b/src/activities/settings/CalibreSettingsActivity.h index 53de46bc6..88bc336d7 100644 --- a/src/activities/settings/CalibreSettingsActivity.h +++ b/src/activities/settings/CalibreSettingsActivity.h @@ -28,7 +28,7 @@ class CalibreSettingsActivity final : public ActivityWithSubactivity { ButtonNavigator buttonNavigator; bool updateRequired = false; - int selectedIndex = 0; + size_t selectedIndex = 0; const std::function onBack; static void taskTrampoline(void* param); diff --git a/src/activities/settings/KOReaderAuthActivity.cpp b/src/activities/settings/KOReaderAuthActivity.cpp index 78d6ec84e..dd63485ed 100644 --- a/src/activities/settings/KOReaderAuthActivity.cpp +++ b/src/activities/settings/KOReaderAuthActivity.cpp @@ -123,34 +123,29 @@ void KOReaderAuthActivity::render() { return; } - renderer.clearScreen(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Auth", true, EpdFontFamily::BOLD); + auto metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); - if (state == AUTHENTICATING) { - renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD); - renderer.displayBuffer(); - return; - } + renderer.clearScreen(); - if (state == SUCCESS) { - renderer.drawCenteredText(UI_10_FONT_ID, 280, "Success!", true, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_10_FONT_ID, 320, "KOReader sync is ready to use"); + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "KOReader Sync"); + const auto height = renderer.getLineHeight(UI_10_FONT_ID); + const auto top = (pageHeight - height) / 2; - const auto labels = mappedInput.mapLabels("Done", "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); - return; + if (state == AUTHENTICATING) { + renderer.drawCenteredText(UI_10_FONT_ID, top, statusMessage.c_str()); + } else if (state == SUCCESS) { + renderer.drawCenteredText(UI_10_FONT_ID, top, "Success!", true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, "KOReader sync is ready to use"); + } else if (state == FAILED) { + renderer.drawCenteredText(UI_10_FONT_ID, top, "Error", true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, errorMessage.c_str()); } - if (state == FAILED) { - renderer.drawCenteredText(UI_10_FONT_ID, 280, "Authentication Failed", true, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_10_FONT_ID, 320, errorMessage.c_str()); - - const auto labels = mappedInput.mapLabels("Back", "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); - return; - } + const auto labels = mappedInput.mapLabels("Back", "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + renderer.displayBuffer(); } void KOReaderAuthActivity::loop() { diff --git a/src/activities/settings/KOReaderSettingsActivity.cpp b/src/activities/settings/KOReaderSettingsActivity.cpp index a72151d62..d61861577 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -172,43 +172,39 @@ void KOReaderSettingsActivity::displayTaskLoop() { } void KOReaderSettingsActivity::render() { - renderer.clearScreen(); - + auto metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); - // Draw header - renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Sync", true, EpdFontFamily::BOLD); - - // Draw selection highlight - renderer.fillRect(0, 60 + selectedIndex * 30 - 2, pageWidth - 1, 30); - - // Draw menu items - for (int i = 0; i < MENU_ITEMS; i++) { - const int settingY = 60 + i * 30; - const bool isSelected = (i == selectedIndex); - - renderer.drawText(UI_10_FONT_ID, 20, settingY, menuNames[i], !isSelected); - - // Draw status for each item - const char* status = ""; - if (i == 0) { - status = KOREADER_STORE.getUsername().empty() ? "[Not Set]" : "[Set]"; - } else if (i == 1) { - status = KOREADER_STORE.getPassword().empty() ? "[Not Set]" : "[Set]"; - } else if (i == 2) { - status = KOREADER_STORE.getServerUrl().empty() ? "[Default]" : "[Custom]"; - } else if (i == 3) { - status = KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? "[Filename]" : "[Binary]"; - } else if (i == 4) { - status = KOREADER_STORE.hasCredentials() ? "" : "[Set credentials first]"; - } - - const auto width = renderer.getTextWidth(UI_10_FONT_ID, status); - renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected); - } + renderer.clearScreen(); - // Draw button hints - const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "KOReader Sync"); + + const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; + GUI.drawList( + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(MENU_ITEMS), + static_cast(selectedIndex), [this](int index) { return menuNames[index]; }, nullptr, nullptr, + [this](int index) { + // Draw status for each setting + if (index == 0) { + auto username = KOREADER_STORE.getUsername(); + return username.empty() ? std::string("[Not Set]") : username; + } else if (index == 1) { + return std::string(KOREADER_STORE.getPassword().empty() ? "[Not Set]" : "******"); + } else if (index == 2) { + auto serverUrl = KOREADER_STORE.getServerUrl(); + return serverUrl.empty() ? std::string("Default") : serverUrl; + } else if (index == 3) { + return std::string(KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? "Filename" : "Binary"); + } else if (index == 4) { + return std::string(KOREADER_STORE.hasCredentials() ? "" : "[Set credentials first]"); + } + return std::string("[Not Set]"); + }); + + // Draw help text at bottom + const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); diff --git a/src/activities/settings/KOReaderSettingsActivity.h b/src/activities/settings/KOReaderSettingsActivity.h index 24f2f820b..fc8cf1b37 100644 --- a/src/activities/settings/KOReaderSettingsActivity.h +++ b/src/activities/settings/KOReaderSettingsActivity.h @@ -28,7 +28,7 @@ class KOReaderSettingsActivity final : public ActivityWithSubactivity { ButtonNavigator buttonNavigator; bool updateRequired = false; - int selectedIndex = 0; + size_t selectedIndex = 0; const std::function onBack; static void taskTrampoline(void* param); diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index ad8888444..7b410920f 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -98,9 +98,9 @@ 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& rowTitle, - const std::function& rowSubtitle, - const std::function& rowIcon, - const std::function& rowValue) const; + const std::function& rowSubtitle = nullptr, + const std::function& rowIcon = nullptr, + const std::function& rowValue = nullptr) 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; diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 642a54996..d1f055d88 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -19,6 +19,7 @@ constexpr int hPaddingInSelection = 8; constexpr int cornerRadius = 6; constexpr int topHintButtonY = 345; constexpr int maxSubtitleWidth = 100; +constexpr int maxListValueWidth = 200; } // namespace void LyraTheme::drawBattery(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { @@ -174,8 +175,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, @@ -189,22 +196,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) { + renderer.fillRoundedRect( + contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - valueWidth, itemY, + valueWidth + hPaddingInSelection, rowHeight, cornerRadius, Color::Black); } + + renderer.drawText(UI_10_FONT_ID, contentWidth - LyraMetrics::values.contentSidePadding - valueWidth, itemY + 6, + valueText.c_str(), i != selectedIndex); } } } From 74890de343f7650da1c230a19d4d31e85867222f Mon Sep 17 00:00:00 2001 From: CaptainFrito Date: Tue, 10 Feb 2026 16:44:43 +0700 Subject: [PATCH 06/11] feat: Lyra theme - Update screen --- src/activities/settings/OtaUpdateActivity.cpp | 78 ++++++++----------- 1 file changed, 33 insertions(+), 45 deletions(-) diff --git a/src/activities/settings/OtaUpdateActivity.cpp b/src/activities/settings/OtaUpdateActivity.cpp index c6f16a788..ce755d4d6 100644 --- a/src/activities/settings/OtaUpdateActivity.cpp +++ b/src/activities/settings/OtaUpdateActivity.cpp @@ -114,6 +114,16 @@ void OtaUpdateActivity::render() { return; } + auto metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + + renderer.clearScreen(); + + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "Update"); + const auto height = renderer.getLineHeight(UI_10_FONT_ID); + const auto top = (pageHeight - height) / 2; + float updaterProgress = 0; if (state == UPDATE_IN_PROGRESS) { Serial.printf("[%lu] [OTA] Update progress: %d / %d\n", millis(), updater.getProcessedSize(), @@ -126,60 +136,38 @@ void OtaUpdateActivity::render() { lastUpdaterPercentage = static_cast(updaterProgress * 100); } - const auto pageWidth = renderer.getScreenWidth(); - - renderer.clearScreen(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Update", true, EpdFontFamily::BOLD); - if (state == CHECKING_FOR_UPDATE) { - renderer.drawCenteredText(UI_10_FONT_ID, 300, "Checking for update...", true, EpdFontFamily::BOLD); - renderer.displayBuffer(); - return; - } - - if (state == WAITING_CONFIRMATION) { - renderer.drawCenteredText(UI_10_FONT_ID, 200, "New update available!", true, EpdFontFamily::BOLD); - renderer.drawText(UI_10_FONT_ID, 20, 250, "Current Version: " CROSSPOINT_VERSION); - renderer.drawText(UI_10_FONT_ID, 20, 270, ("New Version: " + updater.getLatestVersion()).c_str()); + renderer.drawCenteredText(UI_10_FONT_ID, top, "Checking for update..."); + } else if (state == WAITING_CONFIRMATION) { + renderer.drawCenteredText(UI_10_FONT_ID, top, "New update available!", true, EpdFontFamily::BOLD); + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height + 10, + "Current Version: " CROSSPOINT_VERSION); + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height + 30, + ("New Version: " + updater.getLatestVersion()).c_str()); const auto labels = mappedInput.mapLabels("Cancel", "Update", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); - return; - } - - if (state == UPDATE_IN_PROGRESS) { - renderer.drawCenteredText(UI_10_FONT_ID, 310, "Updating...", true, EpdFontFamily::BOLD); - renderer.drawRect(20, 350, pageWidth - 40, 50); - renderer.fillRect(24, 354, static_cast(updaterProgress * static_cast(pageWidth - 44)), 42); - renderer.drawCenteredText(UI_10_FONT_ID, 420, + } else if (state == UPDATE_IN_PROGRESS) { + renderer.drawCenteredText(UI_10_FONT_ID, top, "Updating..."); + renderer.drawRect(metrics.contentSidePadding, top + height + 10, pageWidth - metrics.contentSidePadding * 2, 50); + renderer.fillRect( + metrics.contentSidePadding + 4, top + height + 14, + static_cast(updaterProgress * static_cast(pageWidth - metrics.contentSidePadding * 2 - 8)), 42); + renderer.drawCenteredText(UI_10_FONT_ID, top + height + 70, (std::to_string(static_cast(updaterProgress * 100)) + "%").c_str()); renderer.drawCenteredText( - UI_10_FONT_ID, 440, + UI_10_FONT_ID, top + height * 2 + 80, (std::to_string(updater.getProcessedSize()) + " / " + std::to_string(updater.getTotalSize())).c_str()); - renderer.displayBuffer(); - return; - } - - if (state == NO_UPDATE) { - renderer.drawCenteredText(UI_10_FONT_ID, 300, "No update available", true, EpdFontFamily::BOLD); - renderer.displayBuffer(); - return; + } else if (state == NO_UPDATE) { + renderer.drawCenteredText(UI_10_FONT_ID, top, "No update available", true, EpdFontFamily::BOLD); + } else if (state == FAILED) { + renderer.drawCenteredText(UI_10_FONT_ID, top, "Update failed", true, EpdFontFamily::BOLD); + } else if (state == FINISHED) { + renderer.drawCenteredText(UI_10_FONT_ID, top, "Update complete", true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, "Press and hold power button to turn back on"); } - if (state == FAILED) { - renderer.drawCenteredText(UI_10_FONT_ID, 300, "Update failed", true, EpdFontFamily::BOLD); - renderer.displayBuffer(); - return; - } - - if (state == FINISHED) { - renderer.drawCenteredText(UI_10_FONT_ID, 300, "Update complete", true, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_10_FONT_ID, 350, "Press and hold power button to turn back on"); - renderer.displayBuffer(); - state = SHUTTING_DOWN; - return; - } + renderer.displayBuffer(); } void OtaUpdateActivity::loop() { From adc9e3b1558299b1049cb2ce80e9bcefe416b55f Mon Sep 17 00:00:00 2001 From: CaptainFrito Date: Tue, 10 Feb 2026 16:44:43 +0700 Subject: [PATCH 07/11] feat: Lyra theme - Calibre connect screen --- .../network/CalibreConnectActivity.cpp | 111 ++++++++---------- .../settings/CalibreSettingsActivity.cpp | 3 +- .../settings/KOReaderSettingsActivity.cpp | 3 +- src/activities/settings/OtaUpdateActivity.cpp | 26 ++-- src/activities/settings/SettingsActivity.cpp | 3 +- src/components/themes/BaseTheme.cpp | 17 ++- src/components/themes/BaseTheme.h | 12 +- src/components/themes/lyra/LyraTheme.cpp | 19 ++- src/components/themes/lyra/LyraTheme.h | 8 +- 9 files changed, 111 insertions(+), 91 deletions(-) diff --git a/src/activities/network/CalibreConnectActivity.cpp b/src/activities/network/CalibreConnectActivity.cpp index c7f636d66..92ede6610 100644 --- a/src/activities/network/CalibreConnectActivity.cpp +++ b/src/activities/network/CalibreConnectActivity.cpp @@ -201,75 +201,66 @@ void CalibreConnectActivity::displayTaskLoop() { } void CalibreConnectActivity::render() const { - if (state == CalibreConnectState::SERVER_RUNNING) { - renderer.clearScreen(); - renderServerRunning(); - renderer.displayBuffer(); + if (subActivity) { return; } - renderer.clearScreen(); + auto metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); + + renderer.clearScreen(); + + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "Connect to Calibre"); + const auto height = renderer.getLineHeight(UI_10_FONT_ID); + const auto top = (pageHeight - height) / 2; + if (state == CalibreConnectState::SERVER_STARTING) { - renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Starting Calibre...", true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, top, "Starting Calibre..."); } else if (state == CalibreConnectState::ERROR) { - renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Calibre setup failed", true, EpdFontFamily::BOLD); - } - renderer.displayBuffer(); -} - -void CalibreConnectActivity::renderServerRunning() const { - constexpr int LINE_SPACING = 24; - constexpr int SMALL_SPACING = 20; - constexpr int SECTION_SPACING = 40; - constexpr int TOP_PADDING = 14; - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Connect to Calibre", true, EpdFontFamily::BOLD); - - int y = 55 + TOP_PADDING; - renderer.drawCenteredText(UI_10_FONT_ID, y, "Network", true, EpdFontFamily::BOLD); - y += LINE_SPACING; - std::string ssidInfo = "Network: " + connectedSSID; - if (ssidInfo.length() > 28) { - ssidInfo.replace(25, ssidInfo.length() - 25, "..."); - } - renderer.drawCenteredText(UI_10_FONT_ID, y, ssidInfo.c_str()); - renderer.drawCenteredText(UI_10_FONT_ID, y + LINE_SPACING, ("IP: " + connectedIP).c_str()); - - y += LINE_SPACING * 2 + SECTION_SPACING; - renderer.drawCenteredText(UI_10_FONT_ID, y, "Setup", true, EpdFontFamily::BOLD); - y += LINE_SPACING; - renderer.drawCenteredText(SMALL_FONT_ID, y, "1) Install CrossPoint Reader plugin"); - renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING, "2) Be on the same WiFi network"); - renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 2, "3) In Calibre: \"Send to device\""); - renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 3, "Keep this screen open while sending"); - - y += SMALL_SPACING * 3 + SECTION_SPACING; - renderer.drawCenteredText(UI_10_FONT_ID, y, "Status", true, EpdFontFamily::BOLD); - y += LINE_SPACING; - if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) { - std::string label = "Receiving"; - if (!currentUploadName.empty()) { - label += ": " + currentUploadName; - if (label.length() > 34) { - label.replace(31, label.length() - 31, "..."); + renderer.drawCenteredText(UI_12_FONT_ID, top, "Calibre setup failed", true, EpdFontFamily::BOLD); + } else if (state == CalibreConnectState::SERVER_RUNNING) { + GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, + connectedSSID.c_str(), ("IP: " + connectedIP).c_str()); + + int y = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 4; + const auto heightText12 = renderer.getTextHeight(UI_12_FONT_ID); + renderer.drawText(UI_12_FONT_ID, metrics.contentSidePadding, y, "Setup", true, EpdFontFamily::BOLD); + y += heightText12 + metrics.verticalSpacing * 2; + + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y, "1) Install CrossPoint Reader plugin"); + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height, "2) Be on the same WiFi network"); + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height * 2, "3) In Calibre: \"Send to device\""); + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height * 3, "Keep this screen open while sending"); + + y += height * 3 + metrics.verticalSpacing * 4; + renderer.drawText(UI_12_FONT_ID, metrics.contentSidePadding, y, "Status", true, EpdFontFamily::BOLD); + y += heightText12 + metrics.verticalSpacing * 2; + + if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) { + std::string label = "Receiving"; + if (!currentUploadName.empty()) { + label += ": " + currentUploadName; + label = renderer.truncatedText(SMALL_FONT_ID, label.c_str(), pageWidth - metrics.contentSidePadding * 2, + EpdFontFamily::REGULAR); } + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y, label.c_str()); + GUI.drawProgressBar(renderer, + Rect{metrics.contentSidePadding, y + height + metrics.verticalSpacing, + pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight}, + lastProgressReceived, lastProgressTotal); + y += height + metrics.verticalSpacing * 2 + metrics.progressBarHeight; } - renderer.drawCenteredText(SMALL_FONT_ID, y, label.c_str()); - constexpr int barWidth = 300; - constexpr int barHeight = 16; - constexpr int barX = (480 - barWidth) / 2; - GUI.drawProgressBar(renderer, Rect{barX, y + 22, barWidth, barHeight}, lastProgressReceived, lastProgressTotal); - y += 40; - } - if (lastCompleteAt > 0 && (millis() - lastCompleteAt) < 6000) { - std::string msg = "Received: " + lastCompleteName; - if (msg.length() > 36) { - msg.replace(33, msg.length() - 33, "..."); + if (lastCompleteAt > 0 && (millis() - lastCompleteAt) < 6000) { + std::string msg = "Received: " + lastCompleteName; + msg = renderer.truncatedText(SMALL_FONT_ID, msg.c_str(), pageWidth - metrics.contentSidePadding * 2, + EpdFontFamily::REGULAR); + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y, msg.c_str()); } - renderer.drawCenteredText(SMALL_FONT_ID, y, msg.c_str()); - } - const auto labels = mappedInput.mapLabels("« Exit", "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + const auto labels = mappedInput.mapLabels("« Exit", "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + } + renderer.displayBuffer(); } diff --git a/src/activities/settings/CalibreSettingsActivity.cpp b/src/activities/settings/CalibreSettingsActivity.cpp index e34a0d134..47fe418c2 100644 --- a/src/activities/settings/CalibreSettingsActivity.cpp +++ b/src/activities/settings/CalibreSettingsActivity.cpp @@ -175,7 +175,8 @@ void CalibreSettingsActivity::render() { return (strlen(SETTINGS.opdsPassword) > 0) ? "******" : "[Not Set]"; } return "[Not Set]"; - }); + }, + true); // Draw help text at bottom const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); diff --git a/src/activities/settings/KOReaderSettingsActivity.cpp b/src/activities/settings/KOReaderSettingsActivity.cpp index d61861577..3edb935ec 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -201,7 +201,8 @@ void KOReaderSettingsActivity::render() { return std::string(KOREADER_STORE.hasCredentials() ? "" : "[Set credentials first]"); } return std::string("[Not Set]"); - }); + }, + true); // Draw help text at bottom const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); diff --git a/src/activities/settings/OtaUpdateActivity.cpp b/src/activities/settings/OtaUpdateActivity.cpp index ce755d4d6..c25aae4e0 100644 --- a/src/activities/settings/OtaUpdateActivity.cpp +++ b/src/activities/settings/OtaUpdateActivity.cpp @@ -61,7 +61,7 @@ void OtaUpdateActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); xTaskCreate(&OtaUpdateActivity::taskTrampoline, "OtaUpdateActivityTask", - 2048, // Stack size + 4096, // Stack size this, // Parameters 1, // Priority &displayTaskHandle // Task handle @@ -140,23 +140,28 @@ void OtaUpdateActivity::render() { renderer.drawCenteredText(UI_10_FONT_ID, top, "Checking for update..."); } else if (state == WAITING_CONFIRMATION) { renderer.drawCenteredText(UI_10_FONT_ID, top, "New update available!", true, EpdFontFamily::BOLD); - renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height + 10, + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height + metrics.verticalSpacing, "Current Version: " CROSSPOINT_VERSION); - renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height + 30, + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height * 2 + metrics.verticalSpacing * 2, ("New Version: " + updater.getLatestVersion()).c_str()); const auto labels = mappedInput.mapLabels("Cancel", "Update", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } else if (state == UPDATE_IN_PROGRESS) { renderer.drawCenteredText(UI_10_FONT_ID, top, "Updating..."); - renderer.drawRect(metrics.contentSidePadding, top + height + 10, pageWidth - metrics.contentSidePadding * 2, 50); - renderer.fillRect( - metrics.contentSidePadding + 4, top + height + 14, - static_cast(updaterProgress * static_cast(pageWidth - metrics.contentSidePadding * 2 - 8)), 42); - renderer.drawCenteredText(UI_10_FONT_ID, top + height + 70, + + int y = top + height + metrics.verticalSpacing; + GUI.drawProgressBar( + renderer, + Rect{metrics.contentSidePadding, y, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight}, + static_cast(updaterProgress * 100), 100); + + y += metrics.progressBarHeight + metrics.verticalSpacing; + renderer.drawCenteredText(UI_10_FONT_ID, y, (std::to_string(static_cast(updaterProgress * 100)) + "%").c_str()); + y += height + metrics.verticalSpacing; renderer.drawCenteredText( - UI_10_FONT_ID, top + height * 2 + 80, + UI_10_FONT_ID, y, (std::to_string(updater.getProcessedSize()) + " / " + std::to_string(updater.getTotalSize())).c_str()); } else if (state == NO_UPDATE) { renderer.drawCenteredText(UI_10_FONT_ID, top, "No update available", true, EpdFontFamily::BOLD); @@ -164,7 +169,8 @@ void OtaUpdateActivity::render() { renderer.drawCenteredText(UI_10_FONT_ID, top, "Update failed", true, EpdFontFamily::BOLD); } else if (state == FINISHED) { renderer.drawCenteredText(UI_10_FONT_ID, top, "Update complete", true, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, "Press and hold power button to turn back on"); + renderer.drawCenteredText(UI_10_FONT_ID, top + height + metrics.verticalSpacing, + "Press and hold power button to turn back on"); } renderer.displayBuffer(); diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index b1bdbb752..e7e6238cf 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -279,7 +279,8 @@ void SettingsActivity::render() const { valueText = std::to_string(SETTINGS.*(settings[i].valuePtr)); } return valueText; - }); + }, + true); // Draw help text const auto labels = mappedInput.mapLabels("« Back", "Toggle", "Up", "Down"); diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 8b9bd183f..bd8253fd6 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -64,6 +64,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((static_cast(current) * 100) / total); + Serial.printf("Drawing progress bar: current=%u, total=%u, percent=%d\n", current, total, percent); // Draw outline renderer.drawRect(rect.x, rect.y, rect.width, rect.height); @@ -162,7 +163,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, const std::function& rowTitle, const std::function& rowSubtitle, const std::function& rowIcon, - const std::function& rowValue) const { + const std::function& rowValue, bool highlightValue) const { int rowHeight = (rowSubtitle != nullptr) ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight; int pageItems = rect.height / rowHeight; @@ -258,14 +259,24 @@ void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t } } -void BaseTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label) const { +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 * 2, EpdFontFamily::REGULAR); + 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); } diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index 7b410920f..53f630e40 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -51,9 +51,7 @@ struct ThemeMetrics { int buttonHintsHeight; int sideButtonHintsWidth; - int versionTextRightX; - int versionTextY; - + int progressBarHeight; int bookProgressBarHeight; }; @@ -82,7 +80,7 @@ constexpr ThemeMetrics values = {.batteryWidth = 15, .homeRecentBooksCount = 1, .buttonHintsHeight = 40, .sideButtonHintsWidth = 30, - .versionTextRightX = 20, + .progressBarHeight = 16, .bookProgressBarHeight = 4}; } @@ -100,10 +98,12 @@ class BaseTheme { const std::function& rowTitle, const std::function& rowSubtitle = nullptr, const std::function& rowIcon = nullptr, - const std::function& rowValue = nullptr) const; + const std::function& 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; + 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& tabs, bool selected) const; virtual void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector& recentBooks, diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index d1f055d88..760207700 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -94,11 +94,20 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t } } -void LyraTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label) const { +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 * 2, EpdFontFamily::REGULAR); + 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); @@ -140,7 +149,7 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, const std::function& rowTitle, const std::function& rowSubtitle, const std::function& rowIcon, - const std::function& rowValue) const { + const std::function& rowValue, bool highlightValue) const { int rowHeight = (rowSubtitle != nullptr) ? LyraMetrics::values.listWithSubtitleRowHeight : LyraMetrics::values.listRowHeight; int pageItems = rect.height / rowHeight; @@ -198,14 +207,14 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, // Draw value if (!valueText.empty()) { - if (i == selectedIndex) { + 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, contentWidth - LyraMetrics::values.contentSidePadding - valueWidth, itemY + 6, - valueText.c_str(), i != selectedIndex); + valueText.c_str(), !(i == selectedIndex && highlightValue)); } } } diff --git a/src/components/themes/lyra/LyraTheme.h b/src/components/themes/lyra/LyraTheme.h index 41ea670ec..8a7b6d460 100644 --- a/src/components/themes/lyra/LyraTheme.h +++ b/src/components/themes/lyra/LyraTheme.h @@ -27,24 +27,24 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, .homeRecentBooksCount = 3, .buttonHintsHeight = 40, .sideButtonHintsWidth = 30, - .versionTextRightX = 20, + .progressBarHeight = 16, .bookProgressBarHeight = 4}; } class LyraTheme : public BaseTheme { public: // Component drawing methods - // void drawProgressBar(const GfxRenderer& renderer, Rect rect, size_t current, size_t total) override; void drawBattery(const GfxRenderer& renderer, Rect rect, bool showPercentage = true) 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 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& tabs, bool selected) const override; void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, const std::function& rowTitle, const std::function& rowSubtitle, const std::function& rowIcon, - const std::function& rowValue) const override; + const std::function& 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; From 25b335d3d487a655ad7f544efac29fc86f09f723 Mon Sep 17 00:00:00 2001 From: CaptainFrito Date: Tue, 10 Feb 2026 16:44:43 +0700 Subject: [PATCH 08/11] feat: Lyra theme - Button remap screen --- .../settings/ButtonRemapActivity.cpp | 51 ++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/activities/settings/ButtonRemapActivity.cpp b/src/activities/settings/ButtonRemapActivity.cpp index 43184735a..fc9e24428 100644 --- a/src/activities/settings/ButtonRemapActivity.cpp +++ b/src/activities/settings/ButtonRemapActivity.cpp @@ -126,9 +126,6 @@ void ButtonRemapActivity::loop() { } void ButtonRemapActivity::render() { - renderer.clearScreen(); - - const auto pageWidth = renderer.getScreenWidth(); const auto labelForHardware = [&](uint8_t hardwareIndex) -> const char* { for (uint8_t i = 0; i < kRoleCount; i++) { if (tempMapping[i] == hardwareIndex) { @@ -138,35 +135,41 @@ void ButtonRemapActivity::render() { return "-"; }; - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Remap Front Buttons", true, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_10_FONT_ID, 40, "Press a front button for each role"); - - for (uint8_t i = 0; i < kRoleCount; i++) { - const int y = 70 + i * 30; - const bool isSelected = (i == currentStep); - - // Highlight the role that is currently being assigned. - if (isSelected) { - renderer.fillRect(0, y - 2, pageWidth - 1, 30); - } + auto metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); - const char* roleName = getRoleName(i); - renderer.drawText(UI_10_FONT_ID, 20, y, roleName, !isSelected); + renderer.clearScreen(); - // Show currently assigned hardware button (or unassigned). - const char* assigned = (tempMapping[i] == kUnassigned) ? "Unassigned" : getHardwareName(tempMapping[i]); - const auto width = renderer.getTextWidth(UI_10_FONT_ID, assigned); - renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, y, assigned, !isSelected); - } + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "Remap Front Buttons"); + GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, + "Press a front button for each role"); + + int topOffset = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing; + int contentHeight = pageHeight - topOffset - metrics.buttonHintsHeight - metrics.verticalSpacing; + GUI.drawList( + renderer, Rect{0, topOffset, pageWidth, contentHeight}, kRoleCount, currentStep, + [&](int index) { return getRoleName(static_cast(index)); }, nullptr, nullptr, + [&](int index) { + uint8_t assignedButton = tempMapping[static_cast(index)]; + return (assignedButton == kUnassigned) ? "Unassigned" : getHardwareName(assignedButton); + }, + true); // Temporary warning banner for duplicates. if (!errorMessage.empty()) { - renderer.drawCenteredText(UI_10_FONT_ID, 210, errorMessage.c_str(), true); + GUI.drawHelpText(renderer, + Rect{0, pageHeight - metrics.buttonHintsHeight - metrics.contentSidePadding - 15, pageWidth, 20}, + errorMessage.c_str()); } // Provide side button actions at the bottom of the screen (split across two lines). - renderer.drawCenteredText(SMALL_FONT_ID, 250, "Side button Up: Reset to default layout", true); - renderer.drawCenteredText(SMALL_FONT_ID, 280, "Side button Down: Cancel remapping", true); + GUI.drawHelpText(renderer, + Rect{0, topOffset + 4 * metrics.listRowHeight + 4 * metrics.verticalSpacing, pageWidth, 20}, + "Side button Up: Reset to default layout"); + GUI.drawHelpText(renderer, + Rect{0, topOffset + 4 * metrics.listRowHeight + 5 * metrics.verticalSpacing + 20, pageWidth, 20}, + "Side button Down: Cancel remapping"); // Live preview of logical labels under front buttons. // This mirrors the on-device front button order: Back, Confirm, Left, Right. From 7691b71428b9ca252b119977212bea115431feb8 Mon Sep 17 00:00:00 2001 From: CaptainFrito Date: Tue, 10 Feb 2026 16:44:43 +0700 Subject: [PATCH 09/11] feat: Lyra theme - Clear cache screen --- src/activities/settings/ClearCacheActivity.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/activities/settings/ClearCacheActivity.cpp b/src/activities/settings/ClearCacheActivity.cpp index b17627dc6..a3b89c5e8 100644 --- a/src/activities/settings/ClearCacheActivity.cpp +++ b/src/activities/settings/ClearCacheActivity.cpp @@ -54,10 +54,13 @@ void ClearCacheActivity::displayTaskLoop() { } void ClearCacheActivity::render() { + auto metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); renderer.clearScreen(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Clear Cache", true, EpdFontFamily::BOLD); + + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "Clear Cache"); if (state == WARNING) { renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, "This will clear all cached book data.", true); @@ -73,7 +76,7 @@ void ClearCacheActivity::render() { } if (state == CLEARING) { - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Clearing cache...", true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Clearing cache..."); renderer.displayBuffer(); return; } From c911b2a6c94c9862a0dfe75aefdc01a0fd152b14 Mon Sep 17 00:00:00 2001 From: CaptainFrito Date: Tue, 10 Feb 2026 16:44:43 +0700 Subject: [PATCH 10/11] feat: Lyra theme - Home screen 1 column 1 cover --- src/CrossPointSettings.h | 2 +- src/SettingsList.h | 3 +- src/components/UITheme.cpp | 33 +---- src/components/UITheme.h | 2 - .../themes/lyra/Lyra3CoversTheme.cpp | 102 +++++++++++++++ src/components/themes/lyra/Lyra3CoversTheme.h | 41 ++++++ src/components/themes/lyra/LyraTheme.cpp | 122 +++++++++--------- src/components/themes/lyra/LyraTheme.h | 4 +- 8 files changed, 214 insertions(+), 95 deletions(-) create mode 100644 src/components/themes/lyra/Lyra3CoversTheme.cpp create mode 100644 src/components/themes/lyra/Lyra3CoversTheme.h diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 1348519f5..f18452afe 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -117,7 +117,7 @@ class CrossPointSettings { enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT }; // UI Theme - enum UI_THEME { CLASSIC = 0, LYRA = 1 }; + enum UI_THEME { CLASSIC = 0, LYRA = 1, LYRA_3_COVERS = 2 }; // Sleep screen settings uint8_t sleepScreen = DARK; diff --git a/src/SettingsList.h b/src/SettingsList.h index e493f40f3..228ca7977 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -26,7 +26,8 @@ inline std::vector getSettingsList() { "hideBatteryPercentage", "Display"), SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"}, "refreshFrequency", "Display"), - SettingInfo::Enum("UI Theme", &CrossPointSettings::uiTheme, {"Classic", "Lyra"}, "uiTheme", "Display"), + SettingInfo::Enum("UI Theme", &CrossPointSettings::uiTheme, {"Classic", "Lyra", "Lyra Extended"}, "uiTheme", + "Display"), SettingInfo::Toggle("Sunlight Fading Fix", &CrossPointSettings::fadingFix, "fadingFix", "Display"), // --- Reader --- diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 6be567f5c..358282a39 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -7,8 +7,8 @@ #include "MappedInputManager.h" #include "RecentBooksStore.h" #include "components/themes/BaseTheme.h" +#include "components/themes/lyra/Lyra3CoversTheme.h" #include "components/themes/lyra/LyraTheme.h" - namespace { constexpr int SKIP_PAGE_MS = 700; } // namespace @@ -37,6 +37,11 @@ void UITheme::setTheme(CrossPointSettings::UI_THEME type) { currentTheme = new LyraTheme(); currentMetrics = &LyraMetrics::values; break; + case CrossPointSettings::UI_THEME::LYRA_3_COVERS: + Serial.printf("[%lu] [UI] Using Lyra 3 Covers theme\n", millis()); + currentTheme = new Lyra3CoversTheme(); + currentMetrics = &Lyra3CoversMetrics::values; + break; } } @@ -65,29 +70,3 @@ std::string UITheme::getCoverThumbPath(std::string coverBmpPath, int coverHeight } return coverBmpPath; } - -void UITheme::handleListScrolling(const GfxRenderer& renderer, int listSize, int pageItems, size_t& selectorIndex, - const MappedInputManager& mappedInput, bool& updateRequired) { - const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left) || - mappedInput.wasReleased(MappedInputManager::Button::Up); - const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Right) || - mappedInput.wasReleased(MappedInputManager::Button::Down); - - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; - - if (upReleased) { - if (skipPage) { - selectorIndex = std::max(static_cast((selectorIndex / pageItems - 1) * pageItems), 0); - } else { - selectorIndex = (selectorIndex + listSize - 1) % listSize; - } - updateRequired = true; - } else if (downReleased) { - if (skipPage) { - selectorIndex = std::min(static_cast((selectorIndex / pageItems + 1) * pageItems), listSize - 1); - } else { - selectorIndex = (selectorIndex + 1) % listSize; - } - updateRequired = true; - } -} diff --git a/src/components/UITheme.h b/src/components/UITheme.h index aa4c66a5a..a7fd89ba3 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -23,8 +23,6 @@ class UITheme { static int getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints, bool hasSubtitle); static std::string getCoverThumbPath(std::string coverBmpPath, int coverHeight); - void handleListScrolling(const GfxRenderer& renderer, int listSize, int pageItems, size_t& selectorIndex, - const MappedInputManager& mappedInput, bool& updateRequired); private: const ThemeMetrics* currentMetrics; diff --git a/src/components/themes/lyra/Lyra3CoversTheme.cpp b/src/components/themes/lyra/Lyra3CoversTheme.cpp new file mode 100644 index 000000000..32d890056 --- /dev/null +++ b/src/components/themes/lyra/Lyra3CoversTheme.cpp @@ -0,0 +1,102 @@ +#include "Lyra3CoversTheme.h" + +#include +#include + +#include +#include + +#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& recentBooks, + const int selectorIndex, bool& coverRendered, bool& coverBufferStored, + bool& bufferRestored, std::function 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(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(bitmap.getHeight()); + float coverWidth = static_cast(bitmap.getWidth()); + float ratio = coverWidth / coverHeight; + const float tileRatio = static_cast(tileWidth - 2 * hPaddingInSelection) / + static_cast(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(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); + } + } +} \ No newline at end of file diff --git a/src/components/themes/lyra/Lyra3CoversTheme.h b/src/components/themes/lyra/Lyra3CoversTheme.h new file mode 100644 index 000000000..c02b43206 --- /dev/null +++ b/src/components/themes/lyra/Lyra3CoversTheme.h @@ -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& recentBooks, + const int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored, + std::function storeCoverBuffer) const override; +}; diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 760207700..58020b96d 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -20,6 +20,7 @@ constexpr int cornerRadius = 6; constexpr int topHintButtonY = 345; constexpr int maxSubtitleWidth = 100; constexpr int maxListValueWidth = 200; +int coverWidth = 0; } // namespace void LyraTheme::drawBattery(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { @@ -291,79 +292,77 @@ void LyraTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* top void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector& recentBooks, const int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored, std::function 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(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount); - i++) { - std::string coverPath = recentBooks[i].coverBmpPath; - bool hasCover = true; - int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i; - 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(bitmap.getHeight()); - float coverWidth = static_cast(bitmap.getWidth()); - float ratio = coverWidth / coverHeight; - const float tileRatio = static_cast(tileWidth - 2 * hPaddingInSelection) / - static_cast(LyraMetrics::values.homeCoverHeight); - float cropX = 1.0f - (tileRatio / ratio); - - renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, - tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight, cropX); - } else { - hasCover = false; - } - file.close(); + 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) { + coverWidth = bitmap.getWidth(); + renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth, + LyraMetrics::values.homeCoverHeight); + } else { + hasCover = false; } - } - - if (!hasCover) { - renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, - tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight); + file.close(); } } - coverBufferStored = storeCoverBuffer(); - coverRendered = true; + if (!hasCover) { + renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth, + LyraMetrics::values.homeCoverHeight); + } } - for (int i = 0; i < std::min(static_cast(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount); 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); - - 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); + coverBufferStored = storeCoverBuffer(); + coverRendered = true; + + bool bookSelected = (selectorIndex == 0); + + 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 + 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); } } @@ -371,11 +370,10 @@ void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount const std::function& buttonLabel, const std::function& 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(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; diff --git a/src/components/themes/lyra/LyraTheme.h b/src/components/themes/lyra/LyraTheme.h index 8a7b6d460..c20a42cdb 100644 --- a/src/components/themes/lyra/LyraTheme.h +++ b/src/components/themes/lyra/LyraTheme.h @@ -23,8 +23,8 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, .scrollBarRightOffset = 5, .homeTopPadding = 56, .homeCoverHeight = 226, - .homeCoverTileHeight = 287, - .homeRecentBooksCount = 3, + .homeCoverTileHeight = 242, + .homeRecentBooksCount = 1, .buttonHintsHeight = 40, .sideButtonHintsWidth = 30, .progressBarHeight = 16, From cd57d9a95eeba3cb3e823002a016bf753c0a581f Mon Sep 17 00:00:00 2001 From: Laust Lund Kristensen Date: Wed, 11 Feb 2026 12:06:48 +0100 Subject: [PATCH 11/11] feat: add multiline wrapping to GtxRenderer Added the function wrappedText to GtxRenderer, that returns a vector of strings that do not exceed the with specified. You can specify a max number of lines, and if it that number is exceeded the rest of the text is truncated using truncatedText. An example implementation has been added to LyraTheme for the title. --- lib/GfxRenderer/GfxRenderer.cpp | 74 ++++++++++++++++++++++++ lib/GfxRenderer/GfxRenderer.h | 2 + src/components/themes/lyra/LyraTheme.cpp | 19 +++++- 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 14024bc4a..67f901c6a 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -659,6 +659,80 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const display.displayBuffer(refreshMode, fadingFix); } +std::vector GfxRenderer::wrappedText( + const int fontId, + const char* text, + const int maxWidth, + const int maxLines, + const EpdFontFamily::Style style) const +{ + std::vector lines; + + if (!text || maxWidth <= 0 || maxLines <= 0) + return lines; + + std::string remaining = text; + std::string currentLine; + + while (!remaining.empty()) + { + if ((int)lines.size() == maxLines - 1) + { + std::string lastLine = truncatedText(fontId, remaining.c_str(), maxWidth, style); + lines.push_back(lastLine); + return lines; + } + + // Find next word + size_t spacePos = remaining.find(' '); + std::string word; + + if (spacePos == std::string::npos) + { + word = remaining; + remaining.clear(); + } + else + { + word = remaining.substr(0, spacePos); + remaining.erase(0, spacePos + 1); + } + + std::string testLine = currentLine.empty() + ? word + : currentLine + " " + word; + + if (getTextWidth(fontId, testLine.c_str(), style) <= maxWidth) + { + currentLine = testLine; + } + else + { + if (!currentLine.empty()) + { + lines.push_back(currentLine); + currentLine = word; + } + else + { + // If a single word longer than maxWidth, we will truncate it and return + // this is to avoid complicated splitting rules since they are different + // between languages. This results in an aesthetically pleasing end. + std::string truncated = truncatedText(fontId, word.c_str(), maxWidth, style); + lines.push_back(truncated); + return lines; + } + } + } + + if (!currentLine.empty() && (int)lines.size() < maxLines) + { + lines.push_back(currentLine); + } + + return lines; +} + std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth, const EpdFontFamily::Style style) const { if (!text || maxWidth <= 0) return ""; diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 4540774ee..90016c485 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -110,6 +110,8 @@ class GfxRenderer { std::string truncatedText(int fontId, const char* text, int maxWidth, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; + std::vector wrappedText(int fontId, const char* text, int maxWidth, int maxLines, + EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; // Helper for drawing rotated text (90 degrees clockwise, for side buttons) void drawTextRotated90CW(int fontId, int x, int y, const char* text, bool black = true, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 58020b96d..5fb9079a1 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -295,6 +295,7 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: const int tileWidth = rect.width - 2 * LyraMetrics::values.contentSidePadding; const int tileHeight = rect.height; const int tileY = rect.y; + const int titleLines = 3; const bool hasContinueReading = !recentBooks.empty(); if (coverWidth == 0) { coverWidth = LyraMetrics::values.homeCoverHeight * 0.6; @@ -356,11 +357,23 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: hPaddingInSelection, cornerRadius, false, false, true, true, Color::LightGray); } - auto title = renderer.truncatedText(UI_12_FONT_ID, book.title.c_str(), textWidth, EpdFontFamily::BOLD); + //auto title = renderer.truncatedText(UI_12_FONT_ID, book.title.c_str(), textWidth, EpdFontFamily::BOLD); + auto title = renderer.wrappedText(UI_12_FONT_ID, book.title.c_str(), textWidth, titleLines, 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); + auto totalTitleTextHeight = title.size() * bookTitleHeight; + int startY = tileY + tileHeight / 2; + + for (size_t i = 0; i < title.size(); ++i) + { + int y = startY - (title.size() - i) * bookTitleHeight; // move upward + renderer.drawText(UI_12_FONT_ID, + tileX + hPaddingInSelection + coverWidth + LyraMetrics::values.verticalSpacing, + y, + title[i].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); }