From 8a11f445718dab07640d1981abf85585124794d2 Mon Sep 17 00:00:00 2001 From: CaptainFrito Date: Mon, 18 May 2026 21:35:27 +0700 Subject: [PATCH] feat: Themed reader menus (#1072) --- .../network/WifiSelectionActivity.cpp | 115 +++++++++--------- .../network/WifiSelectionActivity.h | 17 +-- .../EpubReaderChapterSelectionActivity.cpp | 72 +++-------- .../reader/EpubReaderMenuActivity.cpp | 79 +++++------- .../EpubReaderPercentSelectionActivity.cpp | 20 +-- .../reader/KOReaderSyncActivity.cpp | 57 +++++---- src/components/UITheme.cpp | 44 ++++++- src/components/UITheme.h | 6 + src/components/themes/BaseTheme.cpp | 2 +- src/components/themes/lyra/LyraTheme.cpp | 8 +- 10 files changed, 209 insertions(+), 211 deletions(-) diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index e34403e3..d1c6e1d2 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -472,64 +472,63 @@ void WifiSelectionActivity::render(RenderLock&&) { renderer.clearScreen(); - const auto& metrics = UITheme::getInstance().getMetrics(); - const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); + auto& theme = UITheme::getInstance(); + auto metrics = theme.getMetrics(); + Rect screen = theme.getScreenSafeArea(renderer, true, false); // Draw header char countStr[32]; snprintf(countStr, sizeof(countStr), tr(STR_NETWORKS_FOUND), networks.size()); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_WIFI_NETWORKS), - countStr); - GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, - cachedMacAddress.c_str()); + GUI.drawHeader(renderer, Rect{screen.x, screen.y + metrics.topPadding, screen.width, metrics.headerHeight}, + tr(STR_WIFI_NETWORKS), countStr); + GUI.drawSubHeader( + renderer, + Rect{screen.x, screen.y + metrics.topPadding + metrics.headerHeight, screen.width, metrics.tabBarHeight}, + cachedMacAddress.c_str()); switch (state) { case WifiSelectionState::AUTO_CONNECTING: - renderConnecting(); + renderConnecting(&screen, &metrics); break; case WifiSelectionState::SCANNING: - renderConnecting(); // Reuse connecting screen with different message + renderConnecting(&screen, &metrics); // Reuse connecting screen with different message break; case WifiSelectionState::NETWORK_LIST: - renderNetworkList(); + renderNetworkList(&screen, &metrics); break; case WifiSelectionState::CONNECTING: - renderConnecting(); + renderConnecting(&screen, &metrics); break; case WifiSelectionState::CONNECTED: - renderConnected(); + renderConnected(&screen, &metrics); break; case WifiSelectionState::SAVE_PROMPT: - renderSavePrompt(); + renderSavePrompt(&screen, &metrics); break; case WifiSelectionState::CONNECTION_FAILED: - renderConnectionFailed(); + renderConnectionFailed(&screen, &metrics); break; case WifiSelectionState::FORGET_PROMPT: - renderForgetPrompt(); + renderForgetPrompt(&screen, &metrics); break; } renderer.displayBuffer(); } -void WifiSelectionActivity::renderNetworkList() const { - const auto& metrics = UITheme::getInstance().getMetrics(); - const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); - +void WifiSelectionActivity::renderNetworkList(const Rect* screen, const ThemeMetrics* metrics) const { if (networks.empty()) { // No networks found or scan failed const auto height = renderer.getLineHeight(UI_10_FONT_ID); - const auto top = (pageHeight - height) / 2; - renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NO_NETWORKS)); - renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, tr(STR_PRESS_OK_SCAN)); + const auto top = screen->y + (screen->height - height) / 2; + UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, tr(STR_NO_NETWORKS)); + UITheme::drawCenteredText(renderer, *screen, SMALL_FONT_ID, top + height + 10, tr(STR_PRESS_OK_SCAN)); } else { - int contentTop = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing; - int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; + int contentTop = + screen->y + metrics->topPadding + metrics->headerHeight + metrics->tabBarHeight + metrics->verticalSpacing; + int contentHeight = screen->height - contentTop - metrics->verticalSpacing * 2; GUI.drawList( - renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(networks.size()), + renderer, Rect{screen->x, contentTop, screen->width, contentHeight}, static_cast(networks.size()), selectedNetworkIndex, [this](int index) { return networks[index].ssid; }, nullptr, nullptr, [this](int index) { auto network = networks[index]; @@ -539,7 +538,7 @@ void WifiSelectionActivity::renderNetworkList() const { } GUI.drawHelpText(renderer, - Rect{0, pageHeight - metrics.buttonHintsHeight - metrics.contentSidePadding - 15, pageWidth, 20}, + Rect{screen->x, screen->y + screen->height - metrics->contentSidePadding - 15, screen->width, 20}, tr(STR_NETWORK_LEGEND)); const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword; @@ -549,67 +548,64 @@ void WifiSelectionActivity::renderNetworkList() const { GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } -void WifiSelectionActivity::renderConnecting() const { - const auto pageHeight = renderer.getScreenHeight(); +void WifiSelectionActivity::renderConnecting(const Rect* screen, const ThemeMetrics* metrics) const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); - const auto top = (pageHeight - height) / 2; + const auto top = screen->y + (screen->height - height) / 2; if (state == WifiSelectionState::SCANNING) { - renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_SCANNING)); + UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, tr(STR_SCANNING)); } else { - renderer.drawCenteredText(UI_12_FONT_ID, top - 40, tr(STR_CONNECTING), true, EpdFontFamily::BOLD); + UITheme::drawCenteredText(renderer, *screen, UI_12_FONT_ID, top - 40, tr(STR_CONNECTING), true, + EpdFontFamily::BOLD); std::string ssidInfo = std::string(tr(STR_TO_PREFIX)) + selectedSSID; if (ssidInfo.length() > 25) { ssidInfo.replace(22, ssidInfo.length() - 22, "..."); } - renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str()); + UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, ssidInfo.c_str()); } } -void WifiSelectionActivity::renderConnected() const { - const auto pageHeight = renderer.getScreenHeight(); +void WifiSelectionActivity::renderConnected(const Rect* screen, const ThemeMetrics* metrics) const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); - const auto top = (pageHeight - height * 4) / 2; + const auto top = screen->y + (screen->height - height * 4) / 2; - renderer.drawCenteredText(UI_12_FONT_ID, top - 30, tr(STR_CONNECTED), true, EpdFontFamily::BOLD); + UITheme::drawCenteredText(renderer, *screen, UI_12_FONT_ID, top - 30, tr(STR_CONNECTED), true, EpdFontFamily::BOLD); std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + selectedSSID; if (ssidInfo.length() > 28) { ssidInfo.replace(25, ssidInfo.length() - 25, "..."); } - renderer.drawCenteredText(UI_10_FONT_ID, top + 10, ssidInfo.c_str()); + UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top + 10, ssidInfo.c_str()); const std::string ipInfo = std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP; - renderer.drawCenteredText(UI_10_FONT_ID, top + 40, ipInfo.c_str()); + UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top + 40, ipInfo.c_str()); // Use centralized button hints const auto labels = mappedInput.mapLabels("", tr(STR_DONE), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } -void WifiSelectionActivity::renderSavePrompt() const { - const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); +void WifiSelectionActivity::renderSavePrompt(const Rect* screen, const ThemeMetrics* metrics) const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); - const auto top = (pageHeight - height * 3) / 2; + const auto top = screen->y + (screen->height - height * 3) / 2; - renderer.drawCenteredText(UI_12_FONT_ID, top - 40, tr(STR_CONNECTED), true, EpdFontFamily::BOLD); + UITheme::drawCenteredText(renderer, *screen, UI_12_FONT_ID, top - 40, tr(STR_CONNECTED), true, EpdFontFamily::BOLD); std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + selectedSSID; if (ssidInfo.length() > 28) { ssidInfo.replace(25, ssidInfo.length() - 25, "..."); } - renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str()); + UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, ssidInfo.c_str()); - renderer.drawCenteredText(UI_10_FONT_ID, top + 40, tr(STR_SAVE_PASSWORD)); + UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top + 40, tr(STR_SAVE_PASSWORD)); // Draw Yes/No buttons const int buttonY = top + 80; constexpr int buttonWidth = 60; constexpr int buttonSpacing = 30; constexpr int totalWidth = buttonWidth * 2 + buttonSpacing; - const int startX = (pageWidth - totalWidth) / 2; + const int startX = screen->x + (screen->width - totalWidth) / 2; // Draw "Yes" button if (savePromptSelection == 0) { @@ -632,41 +628,40 @@ void WifiSelectionActivity::renderSavePrompt() const { GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } -void WifiSelectionActivity::renderConnectionFailed() const { - const auto pageHeight = renderer.getScreenHeight(); +void WifiSelectionActivity::renderConnectionFailed(const Rect* screen, const ThemeMetrics* metrics) const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); - const auto top = (pageHeight - height * 2) / 2; + const auto top = screen->y + (screen->height - height * 2) / 2; - renderer.drawCenteredText(UI_12_FONT_ID, top - 20, tr(STR_CONNECTION_FAILED), true, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_10_FONT_ID, top + 20, connectionError.c_str()); + UITheme::drawCenteredText(renderer, *screen, UI_12_FONT_ID, top - 20, tr(STR_CONNECTION_FAILED), true, + EpdFontFamily::BOLD); + UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top + 20, connectionError.c_str()); // Use centralized button hints const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_DONE), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } -void WifiSelectionActivity::renderForgetPrompt() const { - const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); +void WifiSelectionActivity::renderForgetPrompt(const Rect* screen, const ThemeMetrics* metrics) const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); - const auto top = (pageHeight - height * 3) / 2; + const auto top = screen->y + (screen->height - height * 3) / 2; - renderer.drawCenteredText(UI_12_FONT_ID, top - 40, tr(STR_FORGET_NETWORK), true, EpdFontFamily::BOLD); + UITheme::drawCenteredText(renderer, *screen, UI_12_FONT_ID, top - 40, tr(STR_FORGET_NETWORK), true, + EpdFontFamily::BOLD); std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + selectedSSID; if (ssidInfo.length() > 28) { ssidInfo.replace(25, ssidInfo.length() - 25, "..."); } - renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str()); + UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top, ssidInfo.c_str()); - renderer.drawCenteredText(UI_10_FONT_ID, top + 40, tr(STR_FORGET_AND_REMOVE)); + UITheme::drawCenteredText(renderer, *screen, UI_10_FONT_ID, top + 40, tr(STR_FORGET_AND_REMOVE)); // Draw Cancel/Forget network buttons const int buttonY = top + 80; constexpr int buttonWidth = 120; constexpr int buttonSpacing = 30; constexpr int totalWidth = buttonWidth * 2 + buttonSpacing; - const int startX = (pageWidth - totalWidth) / 2; + const int startX = screen->x + (screen->width - totalWidth) / 2; // Draw "Cancel" button if (forgetPromptSelection == 0) { diff --git a/src/activities/network/WifiSelectionActivity.h b/src/activities/network/WifiSelectionActivity.h index 1ea7fc0a..baacf4da 100644 --- a/src/activities/network/WifiSelectionActivity.h +++ b/src/activities/network/WifiSelectionActivity.h @@ -9,6 +9,9 @@ #include "activities/Activity.h" #include "util/ButtonNavigator.h" +struct Rect; +struct ThemeMetrics; + // Structure to hold WiFi network information struct WifiNetworkInfo { std::string ssid; @@ -80,13 +83,13 @@ class WifiSelectionActivity final : public Activity { static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000; unsigned long connectionStartTime = 0; - void renderNetworkList() const; - void renderPasswordEntry() const; - void renderConnecting() const; - void renderConnected() const; - void renderSavePrompt() const; - void renderConnectionFailed() const; - void renderForgetPrompt() const; + void renderNetworkList(const Rect* screen, const ThemeMetrics* metrics) const; + void renderPasswordEntry(const Rect* screen, const ThemeMetrics* metrics) const; + void renderConnecting(const Rect* screen, const ThemeMetrics* metrics) const; + void renderConnected(const Rect* screen, const ThemeMetrics* metrics) const; + void renderSavePrompt(const Rect* screen, const ThemeMetrics* metrics) const; + void renderConnectionFailed(const Rect* screen, const ThemeMetrics* metrics) const; + void renderForgetPrompt(const Rect* screen, const ThemeMetrics* metrics) const; void startWifiScan(); void processWifiScanResults(); diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index ea68bd8f..0f085dce 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -9,22 +9,6 @@ int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub->getTocItemsCount(); } -int EpubReaderChapterSelectionActivity::getPageItems() const { - // Layout constants used in renderScreen - constexpr int lineHeight = 30; - - const int screenHeight = renderer.getScreenHeight(); - const auto orientation = renderer.getOrientation(); - // In inverted portrait, the button hints are drawn near the logical top. - // Reserve vertical space so list items do not collide with the hints. - const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; - const int hintGutterHeight = isPortraitInverted ? 50 : 0; - const int startY = 60 + hintGutterHeight; - const int availableHeight = screenHeight - startY - lineHeight; - // Clamp to at least one item to avoid division by zero and empty paging. - return std::max(1, availableHeight / lineHeight); -} - void EpubReaderChapterSelectionActivity::onEnter() { Activity::onEnter(); @@ -44,7 +28,7 @@ void EpubReaderChapterSelectionActivity::onEnter() { void EpubReaderChapterSelectionActivity::onExit() { Activity::onExit(); } void EpubReaderChapterSelectionActivity::loop() { - const int pageItems = getPageItems(); + const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false); const int totalItems = getTotalItems(); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { @@ -89,46 +73,22 @@ void EpubReaderChapterSelectionActivity::loop() { void EpubReaderChapterSelectionActivity::render(RenderLock&&) { renderer.clearScreen(); - const auto pageWidth = renderer.getScreenWidth(); - const auto orientation = renderer.getOrientation(); - // Landscape orientation: reserve a horizontal gutter for button hints. - const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise; - const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise; - // Inverted portrait: reserve vertical space for hints at the top. - const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; - const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0; - // Landscape CW places hints on the left edge; CCW keeps them on the right. - const int contentX = isLandscapeCw ? hintGutterWidth : 0; - const int contentWidth = pageWidth - hintGutterWidth; - const int hintGutterHeight = isPortraitInverted ? 50 : 0; - const int contentY = hintGutterHeight; - const int pageItems = getPageItems(); + auto metrics = UITheme::getInstance().getMetrics(); + Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + + GUI.drawHeader(renderer, Rect{screen.x, screen.y + metrics.topPadding, screen.width, metrics.headerHeight}, + tr(STR_SELECT_CHAPTER)); + + const int contentTop = screen.y + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + const int contentHeight = screen.height - contentTop - metrics.verticalSpacing; + const int totalItems = getTotalItems(); - - // Manual centering to honor content gutters. - const int titleX = - contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, tr(STR_SELECT_CHAPTER), EpdFontFamily::BOLD)) / 2; - renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, tr(STR_SELECT_CHAPTER), true, EpdFontFamily::BOLD); - - const auto pageStartIndex = selectorIndex / pageItems * pageItems; - // Highlight only the content area, not the hint gutters. - renderer.fillRect(contentX, 60 + contentY + (selectorIndex % pageItems) * 30 - 2, contentWidth - 1, 30); - - for (int i = 0; i < pageItems; i++) { - int itemIndex = pageStartIndex + i; - if (itemIndex >= totalItems) break; - const int displayY = 60 + contentY + i * 30; - const bool isSelected = (itemIndex == selectorIndex); - - auto item = epub->getTocItem(itemIndex); - - // Indent per TOC level while keeping content within the gutter-safe region. - const int indentSize = contentX + 20 + (item.level - 1) * 15; - const std::string chapterName = - renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), contentWidth - 40 - indentSize); - - renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected); - } + GUI.drawList(renderer, Rect{screen.x, contentTop, screen.width, contentHeight}, totalItems, selectorIndex, + [this](int index) { + auto item = epub->getTocItem(index); + std::string indent((item.level - 1) * 2, ' '); + return indent + item.title; + }); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 1d95d9b7..23b3fafe 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -86,29 +86,12 @@ void EpubReaderMenuActivity::loop() { void EpubReaderMenuActivity::render(RenderLock&&) { renderer.clearScreen(); - const auto pageWidth = renderer.getScreenWidth(); - const auto orientation = renderer.getOrientation(); - // Landscape orientation: button hints are drawn along a vertical edge, so we - // reserve a horizontal gutter to prevent overlap with menu content. - const bool isLandscapeCw = orientation == GfxRenderer::Orientation::LandscapeClockwise; - const bool isLandscapeCcw = orientation == GfxRenderer::Orientation::LandscapeCounterClockwise; - // Inverted portrait: button hints appear near the logical top, so we reserve - // vertical space to keep the header and list clear. - const bool isPortraitInverted = orientation == GfxRenderer::Orientation::PortraitInverted; - const int hintGutterWidth = (isLandscapeCw || isLandscapeCcw) ? 30 : 0; - // Landscape CW places hints on the left edge; CCW keeps them on the right. - const int contentX = isLandscapeCw ? hintGutterWidth : 0; - const int contentWidth = pageWidth - hintGutterWidth; - const int hintGutterHeight = isPortraitInverted ? 50 : 0; - const int contentY = hintGutterHeight; - // Title - const std::string truncTitle = - renderer.truncatedText(UI_12_FONT_ID, title.c_str(), contentWidth - 40, EpdFontFamily::BOLD); - // Manual centering so we can respect the content gutter. - const int titleX = - contentX + (contentWidth - renderer.getTextWidth(UI_12_FONT_ID, truncTitle.c_str(), EpdFontFamily::BOLD)) / 2; - renderer.drawText(UI_12_FONT_ID, titleX, 15 + contentY, truncTitle.c_str(), true, EpdFontFamily::BOLD); + auto metrics = UITheme::getInstance().getMetrics(); + Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + + GUI.drawHeader(renderer, Rect{screen.x, screen.y + metrics.topPadding, screen.width, metrics.headerHeight}, + title.c_str()); // Progress summary std::string progressLine; @@ -117,37 +100,31 @@ void EpubReaderMenuActivity::render(RenderLock&&) { std::to_string(totalPages) + std::string(tr(STR_PAGES_SEPARATOR)); } progressLine += std::string(tr(STR_BOOK_PREFIX)) + std::to_string(bookProgressPercent) + "%"; - renderer.drawCenteredText(UI_10_FONT_ID, 45, progressLine.c_str()); + GUI.drawSubHeader( + renderer, + Rect{screen.x, screen.y + metrics.topPadding + metrics.headerHeight, screen.width, metrics.tabBarHeight}, + progressLine.c_str()); - // Menu Items - const int startY = 75 + contentY; - constexpr int lineHeight = 30; + const int contentTop = + screen.y + metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing; + const int contentHeight = screen.height - contentTop - metrics.verticalSpacing; - for (size_t i = 0; i < menuItems.size(); ++i) { - const int displayY = startY + (i * lineHeight); - const bool isSelected = (static_cast(i) == selectedIndex); - - if (isSelected) { - // Highlight only the content area so we don't paint over hint gutters. - renderer.fillRect(contentX, displayY, contentWidth - 1, lineHeight, true); - } - - renderer.drawText(UI_10_FONT_ID, contentX + 20, displayY, I18N.get(menuItems[i].labelId), !isSelected); - - if (menuItems[i].action == MenuAction::ROTATE_SCREEN) { - // Render current orientation value on the right edge of the content area. - const char* value = I18N.get(orientationLabels[pendingOrientation]); - const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); - renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected); - } - - if (menuItems[i].action == MenuAction::AUTO_PAGE_TURN) { - // Render current page turn value on the right edge of the content area. - const auto value = pageTurnLabels[selectedPageTurnOption]; - const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); - renderer.drawText(UI_10_FONT_ID, contentX + contentWidth - 20 - width, displayY, value, !isSelected); - } - } + GUI.drawList( + renderer, Rect{screen.x, contentTop, screen.width, contentHeight}, menuItems.size(), selectedIndex, + [this](int index) { return I18N.get(menuItems[index].labelId); }, nullptr, nullptr, + [this](int index) { + const auto value = menuItems[index].action; + if (value == MenuAction::ROTATE_SCREEN) { + // Render current orientation value on the right edge of the content area. + return I18N.get(orientationLabels[pendingOrientation]); + } else if (value == MenuAction::AUTO_PAGE_TURN) { + // Render current page turn value on the right edge of the content area. + return pageTurnLabels[selectedPageTurnOption]; + } else { + return ""; + } + }, + true); // Footer / Hints const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); diff --git a/src/activities/reader/EpubReaderPercentSelectionActivity.cpp b/src/activities/reader/EpubReaderPercentSelectionActivity.cpp index db4109f0..78cb2683 100644 --- a/src/activities/reader/EpubReaderPercentSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderPercentSelectionActivity.cpp @@ -58,18 +58,24 @@ void EpubReaderPercentSelectionActivity::loop() { void EpubReaderPercentSelectionActivity::render(RenderLock&&) { renderer.clearScreen(); - // Title and numeric percent value. - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_GO_TO_PERCENT), true, EpdFontFamily::BOLD); + auto& theme = UITheme::getInstance(); + auto metrics = theme.getMetrics(); + Rect screen = theme.getScreenSafeArea(renderer, true, false); + + GUI.drawHeader(renderer, Rect{screen.x, screen.y + metrics.topPadding, screen.width, metrics.headerHeight}, + tr(STR_GO_TO_PERCENT)); + + const int contentTop = screen.y + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing * 4; const std::string percentText = std::to_string(percent) + "%"; - renderer.drawCenteredText(UI_12_FONT_ID, 90, percentText.c_str(), true, EpdFontFamily::BOLD); + UITheme::drawCenteredText(renderer, screen, UI_12_FONT_ID, contentTop, percentText.c_str(), true, + EpdFontFamily::BOLD); // Draw slider track. - const int screenWidth = renderer.getScreenWidth(); constexpr int barWidth = 360; constexpr int barHeight = 16; - const int barX = (screenWidth - barWidth) / 2; - const int barY = 140; + const int barX = screen.x + (screen.width - barWidth) / 2; + const int barY = contentTop + metrics.verticalSpacing * 2; renderer.drawRect(barX, barY, barWidth, barHeight); @@ -84,7 +90,7 @@ void EpubReaderPercentSelectionActivity::render(RenderLock&&) { renderer.fillRect(knobX, barY - 4, 4, barHeight + 8, true); // Hint text for step sizes. - renderer.drawCenteredText(SMALL_FONT_ID, barY + 30, tr(STR_PERCENT_STEP_HINT), true); + UITheme::drawCenteredText(renderer, screen, SMALL_FONT_ID, barY + 30, tr(STR_PERCENT_STEP_HINT), true); // Button hints follow the current front button layout. const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "-", "+"); diff --git a/src/activities/reader/KOReaderSyncActivity.cpp b/src/activities/reader/KOReaderSyncActivity.cpp index ec4ec845..834c2e6f 100644 --- a/src/activities/reader/KOReaderSyncActivity.cpp +++ b/src/activities/reader/KOReaderSyncActivity.cpp @@ -320,14 +320,20 @@ void KOReaderSyncActivity::onExit() { } void KOReaderSyncActivity::render(RenderLock&&) { - const auto pageWidth = renderer.getScreenWidth(); - renderer.clearScreen(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_KOREADER_SYNC), true, EpdFontFamily::BOLD); + auto metrics = UITheme::getInstance().getMetrics(); + Rect screen = UITheme::getInstance().getScreenSafeArea(renderer, true, false); + + GUI.drawHeader(renderer, Rect{screen.x, screen.y + metrics.topPadding, screen.width, metrics.headerHeight}, + tr(STR_KOREADER_SYNC)); + + int top = screen.y + screen.height / 2 - 40; if (state == NO_CREDENTIALS) { - renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_NO_CREDENTIALS_MSG), true, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_10_FONT_ID, 320, tr(STR_KOREADER_SETUP_HINT)); + UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, top, tr(STR_NO_CREDENTIALS_MSG), true, + EpdFontFamily::BOLD); + UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, top + 40, tr(STR_KOREADER_SETUP_HINT), true, + EpdFontFamily::BOLD); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); @@ -336,14 +342,15 @@ void KOReaderSyncActivity::render(RenderLock&&) { } if (state == SYNCING || state == UPLOADING) { - renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD); + UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, top, statusMessage.c_str(), true, EpdFontFamily::BOLD); renderer.displayBuffer(); return; } if (state == SHOWING_RESULT) { // Show comparison - renderer.drawCenteredText(UI_10_FONT_ID, 120, tr(STR_PROGRESS_FOUND), true, EpdFontFamily::BOLD); + top = screen.y + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_PROGRESS_FOUND), true, EpdFontFamily::BOLD); // Remote chapter name requires Epub (loaded lazily in performSync before this state). const int remoteTocIndex = epub->getTocIndexForSpineIndex(remotePosition.spineIndex); @@ -356,45 +363,47 @@ void KOReaderSyncActivity::render(RenderLock&&) { : (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(currentSpineIndex + 1)); // Remote progress - chapter and page - renderer.drawText(UI_10_FONT_ID, 20, 160, tr(STR_REMOTE_LABEL), true); + renderer.drawText(UI_10_FONT_ID, screen.x + metrics.contentSidePadding, top + 40, tr(STR_REMOTE_LABEL), true); char remoteChapterStr[128]; snprintf(remoteChapterStr, sizeof(remoteChapterStr), " %s", remoteChapter.c_str()); - renderer.drawText(UI_10_FONT_ID, 20, 185, remoteChapterStr); + renderer.drawText(UI_10_FONT_ID, screen.x + metrics.contentSidePadding, top + 65, remoteChapterStr); char remotePageStr[64]; snprintf(remotePageStr, sizeof(remotePageStr), tr(STR_PAGE_OVERALL_FORMAT), remotePosition.pageNumber + 1, remoteProgress.percentage * 100); - renderer.drawText(UI_10_FONT_ID, 20, 210, remotePageStr); + renderer.drawText(UI_10_FONT_ID, screen.x + metrics.contentSidePadding, top + 90, remotePageStr); if (!remoteProgress.device.empty()) { char deviceStr[64]; snprintf(deviceStr, sizeof(deviceStr), tr(STR_DEVICE_FROM_FORMAT), remoteProgress.device.c_str()); - renderer.drawText(UI_10_FONT_ID, 20, 235, deviceStr); + renderer.drawText(UI_10_FONT_ID, screen.x + metrics.contentSidePadding, top + 115, deviceStr); } // Local progress - chapter and page - renderer.drawText(UI_10_FONT_ID, 20, 270, tr(STR_LOCAL_LABEL), true); + renderer.drawText(UI_10_FONT_ID, screen.x + metrics.contentSidePadding, top + 150, tr(STR_LOCAL_LABEL), true); char localChapterStr[128]; snprintf(localChapterStr, sizeof(localChapterStr), " %s", localChapter.c_str()); - renderer.drawText(UI_10_FONT_ID, 20, 295, localChapterStr); + renderer.drawText(UI_10_FONT_ID, screen.x + metrics.contentSidePadding, top + 175, localChapterStr); char localPageStr[64]; snprintf(localPageStr, sizeof(localPageStr), tr(STR_PAGE_TOTAL_OVERALL_FORMAT), currentPage + 1, totalPagesInSpine, localProgress.percentage * 100); - renderer.drawText(UI_10_FONT_ID, 20, 320, localPageStr); + renderer.drawText(UI_10_FONT_ID, screen.x + metrics.contentSidePadding, top + 200, localPageStr); - const int optionY = 350; + const int optionY = top + 230; const int optionHeight = 30; // Apply option if (selectedOption == 0) { - renderer.fillRect(0, optionY - 2, pageWidth - 1, optionHeight); + renderer.fillRect(screen.x, optionY - 2, screen.width - 1, optionHeight); } - renderer.drawText(UI_10_FONT_ID, 20, optionY, tr(STR_APPLY_REMOTE), selectedOption != 0); + renderer.drawText(UI_10_FONT_ID, screen.x + metrics.contentSidePadding, optionY, tr(STR_APPLY_REMOTE), + selectedOption != 0); // Upload option if (selectedOption == 1) { - renderer.fillRect(0, optionY + optionHeight - 2, pageWidth - 1, optionHeight); + renderer.fillRect(screen.x, optionY + optionHeight - 2, screen.width - 1, optionHeight); } - renderer.drawText(UI_10_FONT_ID, 20, optionY + optionHeight, tr(STR_UPLOAD_LOCAL), selectedOption != 1); + renderer.drawText(UI_10_FONT_ID, screen.x + metrics.contentSidePadding, optionY + optionHeight, + tr(STR_UPLOAD_LOCAL), selectedOption != 1); // Bottom button hints const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); @@ -404,8 +413,8 @@ void KOReaderSyncActivity::render(RenderLock&&) { } if (state == NO_REMOTE_PROGRESS) { - renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_NO_REMOTE_MSG), true, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_10_FONT_ID, 320, tr(STR_UPLOAD_PROMPT)); + UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, top, tr(STR_NO_REMOTE_MSG), true, EpdFontFamily::BOLD); + UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, top + 40, tr(STR_UPLOAD_PROMPT)); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_UPLOAD), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); @@ -414,7 +423,7 @@ void KOReaderSyncActivity::render(RenderLock&&) { } if (state == UPLOAD_COMPLETE) { - renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_UPLOAD_SUCCESS), true, EpdFontFamily::BOLD); + UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, top, tr(STR_UPLOAD_SUCCESS), true, EpdFontFamily::BOLD); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); @@ -423,8 +432,8 @@ void KOReaderSyncActivity::render(RenderLock&&) { } if (state == SYNC_FAILED) { - renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_SYNC_FAILED_MSG), true, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_10_FONT_ID, 320, statusMessage.c_str()); + UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, top, tr(STR_SYNC_FAILED_MSG), true, EpdFontFamily::BOLD); + UITheme::drawCenteredText(renderer, screen, UI_10_FONT_ID, top + 40, statusMessage.c_str()); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 012ad26b..7de7f960 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -53,6 +53,7 @@ void UITheme::setTheme(CrossPointSettings::UI_THEME type) { int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints, bool hasSubtitle, int extraReservedHeight) { const ThemeMetrics& metrics = UITheme::getInstance().getMetrics(); + auto orientation = renderer.getOrientation(); int reservedHeight = metrics.topPadding; if (hasHeader) { reservedHeight += metrics.headerHeight + metrics.verticalSpacing; @@ -60,7 +61,8 @@ int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader if (hasTabBar) { reservedHeight += metrics.tabBarHeight; } - if (hasButtonHints) { + if (hasButtonHints && orientation != GfxRenderer::Orientation::LandscapeClockwise && + orientation != GfxRenderer::Orientation::LandscapeCounterClockwise) { reservedHeight += metrics.verticalSpacing + metrics.buttonHintsHeight; } const int availableHeight = renderer.getScreenHeight() - reservedHeight - extraReservedHeight; @@ -68,6 +70,39 @@ int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader return availableHeight / rowHeight; } +// Screen area excluding the button hints +Rect UITheme::getScreenSafeArea(const GfxRenderer& renderer, bool hasFrontButtonHints, bool hasSideButtonHints) { + auto orientation = renderer.getOrientation(); + const int screenWidth = renderer.getScreenWidth(); + const int screenHeight = renderer.getScreenHeight(); + Rect safeArea = Rect{0, 0, screenWidth, screenHeight}; + switch (orientation) { + case GfxRenderer::Orientation::Portrait: + if (hasFrontButtonHints) { + safeArea.height -= currentMetrics->buttonHintsHeight; + } + break; + case GfxRenderer::Orientation::LandscapeClockwise: + if (hasFrontButtonHints) { + safeArea.x += currentMetrics->buttonHintsHeight; + safeArea.width -= currentMetrics->buttonHintsHeight; + } + break; + case GfxRenderer::Orientation::PortraitInverted: + if (hasFrontButtonHints) { + safeArea.y += currentMetrics->buttonHintsHeight; + safeArea.height -= currentMetrics->buttonHintsHeight; + } + break; + case GfxRenderer::Orientation::LandscapeCounterClockwise: + if (hasFrontButtonHints) { + safeArea.width -= currentMetrics->buttonHintsHeight; + } + break; + } + return safeArea; +} + std::string UITheme::getCoverThumbPath(std::string coverBmpPath, int coverHeight) { size_t pos = coverBmpPath.find("[HEIGHT]", 0); if (pos != std::string::npos) { @@ -111,3 +146,10 @@ int UITheme::getProgressBarHeight() { SETTINGS.statusBarProgressBar != CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS; return (showProgressBar ? (((SETTINGS.statusBarProgressBarThickness + 1) * 2) + metrics.progressBarMarginTop) : 0); } + +// Centered text implementation that takes the safe area into account +void UITheme::drawCenteredText(const GfxRenderer& renderer, Rect screen, int fontId, int y, const char* text, + bool black, EpdFontFamily::Style style) { + const int x = screen.x + (screen.width - renderer.getTextWidth(fontId, text, style)) / 2; + renderer.drawText(fontId, x, y, text, black, style); +} diff --git a/src/components/UITheme.h b/src/components/UITheme.h index c4c37235..66602541 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -16,6 +18,10 @@ class UITheme { const ThemeMetrics& getMetrics() const { return *currentMetrics; } const BaseTheme& getTheme() const { return *currentTheme; } + Rect getScreenSafeArea(const GfxRenderer& renderer, bool hasFrontButtonHints = false, + bool hasSideButtonHints = false); + static void drawCenteredText(const GfxRenderer& renderer, Rect screen, int fontId, int y, const char* text, + bool black = true, EpdFontFamily::Style style = EpdFontFamily::REGULAR); void reload(); void setTheme(CrossPointSettings::UI_THEME type); static int getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints, diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 7fd4ab9e..0650948b 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -266,7 +266,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, // Draw selection int contentWidth = rect.width - 5; if (selectedIndex >= 0) { - renderer.fillRect(0, rect.y + selectedIndex % pageItems * rowHeight - 2, rect.width, rowHeight); + renderer.fillRect(rect.x, rect.y + selectedIndex % pageItems * rowHeight - 2, rect.width, rowHeight); } constexpr int maxValueWidth = 200; constexpr int minValueGap = 10; diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 7ee9ad84..1e11f236 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -233,9 +233,9 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, rect.width - (totalPages > 1 ? (LyraMetrics::values.scrollBarWidth + LyraMetrics::values.scrollBarRightOffset) : 1); if (selectedIndex >= 0) { - renderer.fillRoundedRect(LyraMetrics::values.contentSidePadding, rect.y + selectedIndex % pageItems * rowHeight, - contentWidth - LyraMetrics::values.contentSidePadding * 2, rowHeight, cornerRadius, - Color::LightGray); + renderer.fillRoundedRect( + rect.x + LyraMetrics::values.contentSidePadding, rect.y + selectedIndex % pageItems * rowHeight, + contentWidth - LyraMetrics::values.contentSidePadding * 2, rowHeight, cornerRadius, Color::LightGray); } int textX = rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection; @@ -297,7 +297,7 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, if (!valueText.empty()) { if (i == selectedIndex && highlightValue) { renderer.fillRoundedRect( - contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - valueWidth, itemY, + rect.x + contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - valueWidth, itemY, valueWidth + hPaddingInSelection, rowHeight, cornerRadius, Color::Black); }