From 724c1969b9ab4cf318fcbc4f34c1cd30e9827fc8 Mon Sep 17 00:00:00 2001 From: CaptainFrito Date: Thu, 19 Feb 2026 17:16:55 +0700 Subject: [PATCH] feat: Lyra screens (#732) Implements Lyra theme for some more Crosspoint screens: ![IMG_7960 Medium](https://github.com/user-attachments/assets/5d97d91d-e5eb-4296-bbf4-917e142d9095) ![IMG_7961 Medium](https://github.com/user-attachments/assets/02d61964-2632-45ff-83c7-48b95882eb9c) ![IMG_7962 Medium](https://github.com/user-attachments/assets/cf42d20f-3a85-4669-b497-1cac4653fa5a) ![IMG_7963 Medium](https://github.com/user-attachments/assets/a8f59c37-db70-407c-a06d-3e40613a0f55) ![IMG_7964 Medium](https://github.com/user-attachments/assets/0fdaac72-077a-48f6-a8c5-1cd806a58937) ![IMG_7965 Medium](https://github.com/user-attachments/assets/5169f037-8ba8-4488-9a8a-06f5146ec1d9) - A bit of refactoring for list scrolling logic --- While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --------- Co-authored-by: Dave Allie --- lib/GfxRenderer/GfxRenderer.cpp | 26 +-- lib/I18n/I18nKeys.h | 1 + lib/I18n/translations/czech.yaml | 1 + lib/I18n/translations/english.yaml | 1 + lib/I18n/translations/french.yaml | 7 +- lib/I18n/translations/german.yaml | 1 + lib/I18n/translations/portuguese.yaml | 1 + lib/I18n/translations/russian.yaml | 1 + lib/I18n/translations/spanish.yaml | 3 +- lib/I18n/translations/swedish.yaml | 1 + src/CrossPointSettings.h | 2 +- src/SettingsList.h | 3 +- src/activities/home/MyLibraryActivity.cpp | 1 - .../network/CalibreConnectActivity.cpp | 118 ++++++-------- .../network/CrossPointWebServerActivity.cpp | 114 +++++++------ .../network/NetworkModeSelectionActivity.cpp | 31 +--- .../network/WifiSelectionActivity.cpp | 111 ++++--------- .../network/WifiSelectionActivity.h | 2 +- .../settings/ButtonRemapActivity.cpp | 49 +++--- .../settings/CalibreSettingsActivity.cpp | 65 ++++---- .../settings/CalibreSettingsActivity.h | 2 +- .../settings/ClearCacheActivity.cpp | 7 +- .../settings/KOReaderAuthActivity.cpp | 41 +++-- .../settings/KOReaderSettingsActivity.cpp | 69 ++++---- .../settings/KOReaderSettingsActivity.h | 2 +- src/activities/settings/OtaUpdateActivity.cpp | 81 +++++----- src/activities/settings/SettingsActivity.cpp | 11 +- src/activities/util/KeyboardEntryActivity.cpp | 100 ++++++------ src/activities/util/KeyboardEntryActivity.h | 6 +- src/components/UITheme.cpp | 6 + src/components/themes/BaseTheme.cpp | 64 +++++++- src/components/themes/BaseTheme.h | 36 +++-- src/components/themes/lyra/Lyra3CoversTheme.h | 41 +++++ src/components/themes/lyra/LyraTheme.cpp | 151 ++++++++++++------ src/components/themes/lyra/LyraTheme.h | 20 ++- 35 files changed, 645 insertions(+), 531 deletions(-) create mode 100644 src/components/themes/lyra/Lyra3CoversTheme.h diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 2659ec19..c08a9668 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -325,7 +325,9 @@ void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, con return; } - const int maxRadius = std::min({cornerRadius, width / 2, height / 2}); + // Assume if we're not rounding all corners then we are only rounding one side + const int roundedSides = (!roundTopLeft || !roundTopRight || !roundBottomLeft || !roundBottomRight) ? 1 : 2; + const int maxRadius = std::min({cornerRadius, width / roundedSides, height / roundedSides}); if (maxRadius <= 0) { fillRectDither(x, y, width, height, color); return; @@ -336,10 +338,16 @@ void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, con fillRectDither(x + maxRadius + 1, y, horizontalWidth - 2, height, color); } - const int verticalHeight = height - 2 * maxRadius - 2; - if (verticalHeight > 0) { - fillRectDither(x, y + maxRadius + 1, maxRadius + 1, verticalHeight, color); - fillRectDither(x + width - maxRadius - 1, y + maxRadius + 1, maxRadius + 1, verticalHeight, color); + const int leftFillTop = y + (roundTopLeft ? (maxRadius + 1) : 0); + const int leftFillBottom = y + height - 1 - (roundBottomLeft ? (maxRadius + 1) : 0); + if (leftFillBottom >= leftFillTop) { + fillRectDither(x, leftFillTop, maxRadius + 1, leftFillBottom - leftFillTop + 1, color); + } + + const int rightFillTop = y + (roundTopRight ? (maxRadius + 1) : 0); + const int rightFillBottom = y + height - 1 - (roundBottomRight ? (maxRadius + 1) : 0); + if (rightFillBottom >= rightFillTop) { + fillRectDither(x + width - maxRadius - 1, rightFillTop, maxRadius + 1, rightFillBottom - rightFillTop + 1, color); } auto fillArcTemplated = [this](int maxRadius, int cx, int cy, int xDir, int yDir, Color color) { @@ -363,26 +371,18 @@ void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, con if (roundTopLeft) { fillArcTemplated(maxRadius, x + maxRadius, y + maxRadius, -1, -1, color); - } else { - fillRectDither(x, y, maxRadius + 1, maxRadius + 1, color); } if (roundTopRight) { fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + maxRadius, 1, -1, color); - } else { - fillRectDither(x + width - maxRadius - 1, y, maxRadius + 1, maxRadius + 1, color); } if (roundBottomRight) { fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + height - maxRadius - 1, 1, 1, color); - } else { - fillRectDither(x + width - maxRadius - 1, y + height - maxRadius - 1, maxRadius + 1, maxRadius + 1, color); } if (roundBottomLeft) { fillArcTemplated(maxRadius, x + maxRadius, y + height - maxRadius - 1, -1, 1, color); - } else { - fillRectDither(x, y + height - maxRadius - 1, maxRadius + 1, maxRadius + 1, color); } } diff --git a/lib/I18n/I18nKeys.h b/lib/I18n/I18nKeys.h index 323d5741..42e901af 100644 --- a/lib/I18n/I18nKeys.h +++ b/lib/I18n/I18nKeys.h @@ -290,6 +290,7 @@ enum class StrId : uint16_t { STR_UI_THEME, STR_THEME_CLASSIC, STR_THEME_LYRA, + STR_THEME_LYRA_EXTENDED, STR_SUNLIGHT_FADING_FIX, STR_REMAP_FRONT_BUTTONS, STR_OPDS_BROWSER, diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index 647f584b..d1dc01da 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Plná s pruhem kapitol" STR_UI_THEME: "Šablona rozhraní" STR_THEME_CLASSIC: "Klasická" STR_THEME_LYRA: "Lyra" +STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Oprava blednutí na slunci" STR_REMAP_FRONT_BUTTONS: "Přemapovat přední tlačítka" STR_OPDS_BROWSER: "Prohlížeč OPDS" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index fa7e4f08..81ae4eb0 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Full w/ Chapter Bar" STR_UI_THEME: "UI Theme" STR_THEME_CLASSIC: "Classic" STR_THEME_LYRA: "Lyra" +STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix" STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons" STR_OPDS_BROWSER: "OPDS Browser" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index 9ca39276..51c9434c 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -47,7 +47,7 @@ STR_HOW_CONNECT: "Comment voulez-vous vous connecter ?" STR_JOIN_NETWORK: "Connexion à un réseau" STR_CREATE_HOTSPOT: "Créer un point d’accès" STR_JOIN_DESC: "Se connecter à un réseau WiFi existant" -STR_HOTSPOT_DESC: "Créer un réseau WiFi accessible depuis d’autres appareils" +STR_HOTSPOT_DESC: "Créer un réseau WiFi accessible sur d’autres appareils" STR_STARTING_HOTSPOT: "Création du point d’accès en cours…" STR_HOTSPOT_MODE: "Mode point d’accès" STR_CONNECT_WIFI_HINT: "Connectez un appareil à ce réseau WiFi" @@ -80,7 +80,7 @@ STR_CALIBRE_TRANSFER_INTERRUPTED: "Transfert interrompu" STR_CALIBRE_INSTRUCTION_1: "1) Installer le plugin CrossPoint Reader" STR_CALIBRE_INSTRUCTION_2: "2) Se connecter au même réseau WiFi" STR_CALIBRE_INSTRUCTION_3: "3) Dans Calibre : ‘Envoyer vers l’appareil’" -STR_CALIBRE_INSTRUCTION_4: "“Gardez cet écran ouvert pendant le transfert”" +STR_CALIBRE_INSTRUCTION_4: "4) Gardez cet écran ouvert pendant le transfert" STR_CAT_DISPLAY: "Affichage" STR_CAT_READER: "Lecteur" STR_CAT_CONTROLS: "Commandes" @@ -215,7 +215,7 @@ STR_NO_SERVER_URL: "Aucune URL serveur configurée" STR_FETCH_FEED_FAILED: "Échec du téléchargement du flux" STR_PARSE_FEED_FAILED: "Échec de l’analyse du flux" STR_NETWORK_PREFIX: "Réseau : " -STR_IP_ADDRESS_PREFIX: "Adresse IP : " +STR_IP_ADDRESS_PREFIX: "IP : " STR_SCAN_QR_WIFI_HINT: "or scan QR code with your phone to connect to Wifi." STR_ERROR_GENERAL_FAILURE: "Erreur : Échec général" STR_ERROR_NETWORK_NOT_FOUND: "Erreur : Réseau introuvable" @@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Complète + barre chapitre" STR_UI_THEME: "Thème de l’interface" STR_THEME_CLASSIC: "Classique" STR_THEME_LYRA: "Lyra" +STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Amélioration de la lisibilité au soleil" STR_REMAP_FRONT_BUTTONS: "Réassigner les boutons avant" STR_OPDS_BROWSER: "Navigateur OPDS" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index ceb9e4f3..0098e206 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Komplett + Kapitel" STR_UI_THEME: "System-Design" STR_THEME_CLASSIC: "Klassisch" STR_THEME_LYRA: "Lyra" +STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Anti-Verblassen" STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen" STR_OPDS_BROWSER: "OPDS-Browser" diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese.yaml index 6b516fd3..fc3b5456 100644 --- a/lib/I18n/translations/portuguese.yaml +++ b/lib/I18n/translations/portuguese.yaml @@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Completa c/ barra capítulo" STR_UI_THEME: "Tema da interface" STR_THEME_CLASSIC: "Clássico" STR_THEME_LYRA: "Lyra" +STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol" STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais" STR_OPDS_BROWSER: "Navegador OPDS" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index 25110a90..439aaa3d 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Полная + шкала главы" STR_UI_THEME: "Тема интерфейса" STR_THEME_CLASSIC: "Классическая" STR_THEME_LYRA: "Lyra" +STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Компенсация выцветания" STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки" STR_OPDS_BROWSER: "OPDS браузер" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index 73e70e15..54d34385 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -255,7 +255,8 @@ STR_STATUS_BAR_BOOK_ONLY: "Solo progreso del libro" STR_STATUS_BAR_FULL_CHAPTER: "Completa con progreso de capítulos" STR_UI_THEME: "Estilo de pantalla" STR_THEME_CLASSIC: "Clásico" -STR_THEME_LYRA: "LYRA" +STR_THEME_LYRA: "Lyra" +STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Corrección de desvastado por sol" STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales" STR_OPDS_BROWSER: "Navegador opds" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index b25a5b26..4de062e5 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Full w/ Kapitellist" STR_UI_THEME: "Användargränssnittstema" STR_THEME_CLASSIC: "Klassisk" STR_THEME_LYRA: "Lyra" +STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning" STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar" STR_OPDS_BROWSER: "OPDS-webbläsare" diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 31cd644c..edf7bfe7 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -133,7 +133,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 }; // Home screen clock format enum CLOCK_FORMAT { CLOCK_OFF = 0, CLOCK_AMPM = 1, CLOCK_24H = 2, CLOCK_FORMAT_COUNT }; diff --git a/src/SettingsList.h b/src/SettingsList.h index fe39a489..fe254ee7 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -77,7 +77,8 @@ inline std::vector getSettingsList() { {StrId::STR_PAGES_1, StrId::STR_PAGES_5, StrId::STR_PAGES_10, StrId::STR_PAGES_15, StrId::STR_PAGES_30}, "refreshFrequency", StrId::STR_CAT_DISPLAY), SettingInfo::Enum(StrId::STR_UI_THEME, &CrossPointSettings::uiTheme, - {StrId::STR_THEME_CLASSIC, StrId::STR_THEME_LYRA}, "uiTheme", StrId::STR_CAT_DISPLAY), + {StrId::STR_THEME_CLASSIC, StrId::STR_THEME_LYRA, StrId::STR_THEME_LYRA_EXTENDED}, "uiTheme", + StrId::STR_CAT_DISPLAY), SettingInfo::Toggle(StrId::STR_SUNLIGHT_FADING_FIX, &CrossPointSettings::fadingFix, "fadingFix", StrId::STR_CAT_DISPLAY), // --- Clock --- diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index f224277e..a651799f 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -167,7 +167,6 @@ void MyLibraryActivity::loop() { } int listSize = static_cast(files.size()); - buttonNavigator.onNextRelease([this, listSize] { selectorIndex = ButtonNavigator::nextIndex(static_cast(selectorIndex), listSize); requestUpdate(); diff --git a/src/activities/network/CalibreConnectActivity.cpp b/src/activities/network/CalibreConnectActivity.cpp index 74f281c7..056513bc 100644 --- a/src/activities/network/CalibreConnectActivity.cpp +++ b/src/activities/network/CalibreConnectActivity.cpp @@ -169,76 +169,62 @@ void CalibreConnectActivity::loop() { } void CalibreConnectActivity::render(Activity::RenderLock&&) { - if (state == CalibreConnectState::SERVER_RUNNING) { - renderer.clearScreen(); - renderServerRunning(); - renderer.displayBuffer(); - return; - } + auto metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); renderer.clearScreen(); - const auto pageHeight = renderer.getScreenHeight(); + + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CALIBRE_WIRELESS)); + 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, tr(STR_CALIBRE_STARTING), true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, top, tr(STR_CALIBRE_STARTING)); } else if (state == CalibreConnectState::ERROR) { - renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_CONNECTION_FAILED), true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, top, tr(STR_CONNECTION_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(), (std::string(tr(STR_IP_ADDRESS_PREFIX)) + 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, tr(STR_CALIBRE_SETUP), true, EpdFontFamily::BOLD); + y += heightText12 + metrics.verticalSpacing * 2; + + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y, tr(STR_CALIBRE_INSTRUCTION_1)); + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height, tr(STR_CALIBRE_INSTRUCTION_2)); + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height * 2, tr(STR_CALIBRE_INSTRUCTION_3)); + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height * 3, tr(STR_CALIBRE_INSTRUCTION_4)); + + y += height * 3 + metrics.verticalSpacing * 4; + renderer.drawText(UI_12_FONT_ID, metrics.contentSidePadding, y, tr(STR_CALIBRE_STATUS), true, EpdFontFamily::BOLD); + y += heightText12 + metrics.verticalSpacing * 2; + + if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) { + std::string label = tr(STR_CALIBRE_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; + } + + if (lastCompleteAt > 0 && (millis() - lastCompleteAt) < 6000) { + std::string msg = std::string(tr(STR_CALIBRE_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()); + } + + const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", ""); + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } 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, tr(STR_CALIBRE_WIRELESS), true, EpdFontFamily::BOLD); - - int y = 55 + TOP_PADDING; - renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_WIFI_NETWORKS), true, EpdFontFamily::BOLD); - y += LINE_SPACING; - std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + 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, - (std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP).c_str()); - - y += LINE_SPACING * 2 + SECTION_SPACING; - renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_CALIBRE_SETUP), true, EpdFontFamily::BOLD); - y += LINE_SPACING; - renderer.drawCenteredText(SMALL_FONT_ID, y, tr(STR_CALIBRE_INSTRUCTION_1)); - renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING, tr(STR_CALIBRE_INSTRUCTION_2)); - renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 2, tr(STR_CALIBRE_INSTRUCTION_3)); - renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 3, tr(STR_CALIBRE_INSTRUCTION_4)); - - y += SMALL_SPACING * 3 + SECTION_SPACING; - renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_CALIBRE_STATUS), true, EpdFontFamily::BOLD); - y += LINE_SPACING; - if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) { - std::string label = tr(STR_CALIBRE_RECEIVING); - if (!currentUploadName.empty()) { - label += ": " + currentUploadName; - if (label.length() > 34) { - label.replace(31, label.length() - 31, "..."); - } - } - 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 = std::string(tr(STR_CALIBRE_RECEIVED)) + lastCompleteName; - if (msg.length() > 36) { - msg.replace(33, msg.length() - 33, "..."); - } - renderer.drawCenteredText(SMALL_FONT_ID, y, msg.c_str()); - } - - const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); -} diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index 18a33f97..d326c842 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -24,6 +24,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; @@ -339,14 +341,24 @@ void CrossPointWebServerActivity::loop() { void CrossPointWebServerActivity::render(Activity::RenderLock&&) { // 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, tr(STR_STARTING_HOTSPOT), true, EpdFontFamily::BOLD); + + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, + isApMode ? tr(STR_HOTSPOT_MODE) : tr(STR_FILE_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, tr(STR_STARTING_HOTSPOT)); + } renderer.displayBuffer(); } } @@ -374,66 +386,70 @@ 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, tr(STR_FILE_TRANSFER), true, EpdFontFamily::BOLD); + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, + isApMode ? tr(STR_HOTSPOT_MODE) : tr(STR_FILE_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; + int height10 = renderer.getLineHeight(UI_10_FONT_ID); if (isApMode) { - // AP mode display - center the content block - int startY = 55; + // AP mode display + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, startY, tr(STR_CONNECT_WIFI_HINT), true, + EpdFontFamily::BOLD); + startY += height10 + metrics.verticalSpacing * 2; - renderer.drawCenteredText(UI_10_FONT_ID, startY, tr(STR_HOTSPOT_MODE), true, EpdFontFamily::BOLD); - - std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + connectedSSID; - renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ssidInfo.c_str()); - - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 2, tr(STR_CONNECT_WIFI_HINT)); - - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, tr(STR_SCAN_QR_WIFI_HINT)); - // Show QR code for URL + // Show QR code for Wifi const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;"; - drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 4, wifiConfig); + drawQRCode(renderer, metrics.contentSidePadding, startY, wifiConfig); + + // Show network name + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 80, + connectedSSID.c_str()); + + startY += QR_CODE_HEIGHT + 2 * metrics.verticalSpacing; - startY += 6 * 29 + 3 * LINE_SPACING; // Show primary URL (hostname) + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, startY, tr(STR_OPEN_URL_HINT), true, + EpdFontFamily::BOLD); + startY += height10 + metrics.verticalSpacing * 2; + 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); + std::string ipUrl = tr(STR_OR_HTTP_PREFIX) + connectedIP + "/"; + + // Show QR code for URL + drawQRCode(renderer, metrics.contentSidePadding, startY, hostnameUrl); // Show IP address as fallback - std::string ipUrl = std::string(tr(STR_OR_HTTP_PREFIX)) + connectedIP + "/"; - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, ipUrl.c_str()); - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, tr(STR_OPEN_URL_HINT)); + 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 { + startY += metrics.verticalSpacing * 2; + + // STA mode display (original behavior) + // std::string ipInfo = "IP Address: " + connectedIP; + renderer.drawCenteredText(UI_10_FONT_ID, startY, tr(STR_OPEN_URL_HINT), true, EpdFontFamily::BOLD); + startY += height10; + renderer.drawCenteredText(UI_10_FONT_ID, startY, tr(STR_SCAN_QR_HINT), true, EpdFontFamily::BOLD); + startY += height10 + metrics.verticalSpacing * 2; // Show QR code for URL - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, tr(STR_SCAN_QR_HINT)); - drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 7, hostnameUrl); - } else { - // STA mode display (original behavior) - const int startY = 65; - - std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + connectedSSID; - if (ssidInfo.length() > 28) { - ssidInfo.replace(25, ssidInfo.length() - 25, "..."); - } - renderer.drawCenteredText(UI_10_FONT_ID, startY, ssidInfo.c_str()); - - std::string ipInfo = std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP; - renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ipInfo.c_str()); + 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 += height10 + 5; // Also show hostname URL std::string hostnameUrl = std::string(tr(STR_OR_HTTP_PREFIX)) + AP_HOSTNAME + ".local/"; - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str()); - - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, tr(STR_OPEN_URL_HINT)); - - // 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, tr(STR_SCAN_QR_HINT)); + renderer.drawCenteredText(SMALL_FONT_ID, startY, hostnameUrl.c_str(), true); } const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", ""); diff --git a/src/activities/network/NetworkModeSelectionActivity.cpp b/src/activities/network/NetworkModeSelectionActivity.cpp index 466ea220..e5214229 100644 --- a/src/activities/network/NetworkModeSelectionActivity.cpp +++ b/src/activities/network/NetworkModeSelectionActivity.cpp @@ -57,39 +57,24 @@ void NetworkModeSelectionActivity::loop() { void NetworkModeSelectionActivity::render(Activity::RenderLock&&) { 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, tr(STR_FILE_TRANSFER), true, EpdFontFamily::BOLD); - - // Draw subtitle - renderer.drawCenteredText(UI_10_FONT_ID, 50, tr(STR_HOW_CONNECT)); + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_FILE_TRANSFER)); + const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; // Menu items and descriptions static constexpr StrId menuItems[MENU_ITEM_COUNT] = {StrId::STR_JOIN_NETWORK, StrId::STR_CALIBRE_WIRELESS, StrId::STR_CREATE_HOTSPOT}; static constexpr StrId menuDescs[MENU_ITEM_COUNT] = {StrId::STR_JOIN_DESC, StrId::STR_CALIBRE_DESC, StrId::STR_HOTSPOT_DESC}; - // 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; - - for (int i = 0; i < MENU_ITEM_COUNT; i++) { - const int itemY = startY + i * itemHeight; - const bool isSelected = (i == selectedIndex); - - // Draw selection highlight (black fill) for selected item - if (isSelected) { - renderer.fillRect(20, itemY - 2, pageWidth - 40, itemHeight - 6); - } - - // 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, I18N.get(menuItems[i]), /*black=*/!isSelected); - renderer.drawText(SMALL_FONT_ID, 30, itemY + 22, I18N.get(menuDescs[i]), /*black=*/!isSelected); - } + GUI.drawList( + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(MENU_ITEM_COUNT), selectedIndex, + [](int index) { return std::string(I18N.get(menuItems[index])); }, + [](int index) { return std::string(I18N.get(menuDescs[index])); }); // Draw help text at bottom const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", ""); diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 00d5bbae..b5fa85e8 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -150,13 +150,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; + } + return a.rssi > b.rssi; }); WiFi.scanDelete(); @@ -195,7 +194,6 @@ void WifiSelectionActivity::selectNetwork(const int index) { enterNewActivity(new KeyboardEntryActivity( renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD), "", // No initial text - 50, // Y position 64, // Max password length false, // Show password by default (hard keyboard to use) [this](const std::string& text) { @@ -459,15 +457,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::render(Activity::RenderLock&&) { @@ -480,6 +475,18 @@ void WifiSelectionActivity::render(Activity::RenderLock&&) { 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), 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()); + switch (state) { case WifiSelectionState::AUTO_CONNECTING: renderConnecting(); @@ -511,12 +518,10 @@ void WifiSelectionActivity::render(Activity::RenderLock&&) { } 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, tr(STR_WIFI_NETWORKS), true, EpdFontFamily::BOLD); - if (networks.empty()) { // No networks found or scan failed const auto height = renderer.getLineHeight(UI_10_FONT_ID); @@ -524,69 +529,21 @@ void WifiSelectionActivity::renderNetworkList() const { renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NO_NETWORKS)); renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, tr(STR_PRESS_OK_SCAN)); } 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[64]; - snprintf(countStr, sizeof(countStr), tr(STR_NETWORKS_FOUND), networks.size()); - renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 90, countStr); + 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); + }); } - // 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, tr(STR_NETWORK_LEGEND)); + GUI.drawHelpText(renderer, + Rect{0, pageHeight - metrics.buttonHintsHeight - metrics.contentSidePadding - 15, pageWidth, 20}, + tr(STR_NETWORK_LEGEND)); const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword; const char* forgetLabel = hasSavedPassword ? tr(STR_FORGET_BUTTON) : ""; diff --git a/src/activities/network/WifiSelectionActivity.h b/src/activities/network/WifiSelectionActivity.h index 34641f3c..9356383c 100644 --- a/src/activities/network/WifiSelectionActivity.h +++ b/src/activities/network/WifiSelectionActivity.h @@ -45,7 +45,7 @@ class WifiSelectionActivity final : public ActivityWithSubactivity { ButtonNavigator buttonNavigator; WifiSelectionState state = WifiSelectionState::SCANNING; - int selectedNetworkIndex = 0; + size_t selectedNetworkIndex = 0; std::vector networks; const std::function onComplete; diff --git a/src/activities/settings/ButtonRemapActivity.cpp b/src/activities/settings/ButtonRemapActivity.cpp index 1b9a3132..283d03d8 100644 --- a/src/activities/settings/ButtonRemapActivity.cpp +++ b/src/activities/settings/ButtonRemapActivity.cpp @@ -95,9 +95,6 @@ void ButtonRemapActivity::loop() { } void ButtonRemapActivity::render(Activity::RenderLock&&) { - 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) { @@ -107,35 +104,41 @@ void ButtonRemapActivity::render(Activity::RenderLock&&) { return "-"; }; - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_REMAP_FRONT_BUTTONS), true, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_10_FONT_ID, 40, tr(STR_REMAP_PROMPT)); + auto metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); - for (uint8_t i = 0; i < kRoleCount; i++) { - const int y = 70 + i * 30; - const bool isSelected = (i == currentStep); + renderer.clearScreen(); - // Highlight the role that is currently being assigned. - if (isSelected) { - renderer.fillRect(0, y - 2, pageWidth - 1, 30); - } + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_REMAP_FRONT_BUTTONS)); + GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, + tr(STR_REMAP_PROMPT)); - const char* roleName = getRoleName(i); - renderer.drawText(UI_10_FONT_ID, 20, y, roleName, !isSelected); - - // Show currently assigned hardware button (or unassigned). - const char* assigned = (tempMapping[i] == kUnassigned) ? tr(STR_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); - } + 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) ? tr(STR_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, tr(STR_REMAP_RESET_HINT), true); - renderer.drawCenteredText(SMALL_FONT_ID, 280, tr(STR_REMAP_CANCEL_HINT), true); + GUI.drawHelpText(renderer, + Rect{0, topOffset + 4 * metrics.listRowHeight + 4 * metrics.verticalSpacing, pageWidth, 20}, + tr(STR_REMAP_RESET_HINT)); + GUI.drawHelpText(renderer, + Rect{0, topOffset + 4 * metrics.listRowHeight + 5 * metrics.verticalSpacing + 20, pageWidth, 20}, + tr(STR_REMAP_CANCEL_HINT)); // Live preview of logical labels under front buttons. // This mirrors the on-device front button order: Back, Confirm, Left, Right. diff --git a/src/activities/settings/CalibreSettingsActivity.cpp b/src/activities/settings/CalibreSettingsActivity.cpp index 58b0db78..d6981e9f 100644 --- a/src/activities/settings/CalibreSettingsActivity.cpp +++ b/src/activities/settings/CalibreSettingsActivity.cpp @@ -58,7 +58,7 @@ void CalibreSettingsActivity::handleSelection() { // OPDS Server URL exitActivity(); enterNewActivity(new KeyboardEntryActivity( - renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), SETTINGS.opdsServerUrl, 10, + renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), SETTINGS.opdsServerUrl, 127, // maxLength false, // not password [this](const std::string& url) { @@ -76,7 +76,7 @@ void CalibreSettingsActivity::handleSelection() { // Username exitActivity(); enterNewActivity(new KeyboardEntryActivity( - renderer, mappedInput, tr(STR_USERNAME), SETTINGS.opdsUsername, 10, + renderer, mappedInput, tr(STR_USERNAME), SETTINGS.opdsUsername, 63, // maxLength false, // not password [this](const std::string& username) { @@ -94,7 +94,7 @@ void CalibreSettingsActivity::handleSelection() { // Password exitActivity(); enterNewActivity(new KeyboardEntryActivity( - renderer, mappedInput, tr(STR_PASSWORD), SETTINGS.opdsPassword, 10, + renderer, mappedInput, tr(STR_PASSWORD), SETTINGS.opdsPassword, 63, // maxLength false, // not password mode [this](const std::string& password) { @@ -114,42 +114,35 @@ void CalibreSettingsActivity::handleSelection() { void CalibreSettingsActivity::render(Activity::RenderLock&&) { renderer.clearScreen(); + auto metrics = UITheme::getInstance().getMetrics(); const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_OPDS_BROWSER)); + GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, + tr(STR_CALIBRE_URL_HINT)); - // Draw header - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_OPDS_BROWSER), true, EpdFontFamily::BOLD); + 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), [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr, + nullptr, + [this](int index) { + // Draw status for each setting + if (index == 0) { + return (strlen(SETTINGS.opdsServerUrl) > 0) ? std::string(SETTINGS.opdsServerUrl) + : std::string(tr(STR_NOT_SET)); + } else if (index == 1) { + return (strlen(SETTINGS.opdsUsername) > 0) ? std::string(SETTINGS.opdsUsername) + : std::string(tr(STR_NOT_SET)); + } else if (index == 2) { + return (strlen(SETTINGS.opdsPassword) > 0) ? std::string("******") : std::string(tr(STR_NOT_SET)); + } + return std::string(tr(STR_NOT_SET)); + }, + true); - // Draw info text about Calibre - renderer.drawCenteredText(UI_10_FONT_ID, 40, tr(STR_CALIBRE_URL_HINT)); - - // 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, I18N.get(menuNames[i]), !isSelected); - - // Draw status for each setting - std::string status = std::string("[") + tr(STR_NOT_SET) + "]"; - if (i == 0) { - status = (strlen(SETTINGS.opdsServerUrl) > 0) ? std::string("[") + tr(STR_SET) + "]" - : std::string("[") + tr(STR_NOT_SET) + "]"; - } else if (i == 1) { - status = (strlen(SETTINGS.opdsUsername) > 0) ? std::string("[") + tr(STR_SET) + "]" - : std::string("[") + tr(STR_NOT_SET) + "]"; - } else if (i == 2) { - status = (strlen(SETTINGS.opdsPassword) > 0) ? std::string("[") + tr(STR_SET) + "]" - : std::string("[") + tr(STR_NOT_SET) + "]"; - } - const auto width = renderer.getTextWidth(UI_10_FONT_ID, status.c_str()); - renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status.c_str(), !isSelected); - } - - // Draw button hints - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", ""); + 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); renderer.displayBuffer(); diff --git a/src/activities/settings/CalibreSettingsActivity.h b/src/activities/settings/CalibreSettingsActivity.h index 7d940732..7f5d4dcd 100644 --- a/src/activities/settings/CalibreSettingsActivity.h +++ b/src/activities/settings/CalibreSettingsActivity.h @@ -23,7 +23,7 @@ class CalibreSettingsActivity final : public ActivityWithSubactivity { private: ButtonNavigator buttonNavigator; - int selectedIndex = 0; + size_t selectedIndex = 0; const std::function onBack; void handleSelection(); }; diff --git a/src/activities/settings/ClearCacheActivity.cpp b/src/activities/settings/ClearCacheActivity.cpp index 69931185..b07758d0 100644 --- a/src/activities/settings/ClearCacheActivity.cpp +++ b/src/activities/settings/ClearCacheActivity.cpp @@ -19,10 +19,13 @@ void ClearCacheActivity::onEnter() { void ClearCacheActivity::onExit() { ActivityWithSubactivity::onExit(); } void ClearCacheActivity::render(Activity::RenderLock&&) { + auto metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); renderer.clearScreen(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_CLEAR_READING_CACHE), true, EpdFontFamily::BOLD); + + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CLEAR_READING_CACHE)); if (state == WARNING) { renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, tr(STR_CLEAR_CACHE_WARNING_1), true); @@ -38,7 +41,7 @@ void ClearCacheActivity::render(Activity::RenderLock&&) { } if (state == CLEARING) { - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_CLEARING_CACHE), true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_CLEARING_CACHE)); renderer.displayBuffer(); return; } diff --git a/src/activities/settings/KOReaderAuthActivity.cpp b/src/activities/settings/KOReaderAuthActivity.cpp index f205c0e6..1a487636 100644 --- a/src/activities/settings/KOReaderAuthActivity.cpp +++ b/src/activities/settings/KOReaderAuthActivity.cpp @@ -90,33 +90,28 @@ void KOReaderAuthActivity::onExit() { void KOReaderAuthActivity::render(Activity::RenderLock&&) { renderer.clearScreen(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_KOREADER_AUTH), true, EpdFontFamily::BOLD); + + auto metrics = UITheme::getInstance().getMetrics(); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_KOREADER_AUTH)); + const auto height = renderer.getLineHeight(UI_10_FONT_ID); + const auto top = (pageHeight - height) / 2; if (state == AUTHENTICATING) { - renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD); - renderer.displayBuffer(); - return; + renderer.drawCenteredText(UI_10_FONT_ID, top, statusMessage.c_str()); + } else if (state == SUCCESS) { + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_SUCCESS), true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, tr(STR_SYNC_READY)); + } else if (state == FAILED) { + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_FAILED), true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, errorMessage.c_str()); } - if (state == SUCCESS) { - renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_AUTH_SUCCESS), true, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_10_FONT_ID, 320, tr(STR_SYNC_READY)); - - const auto labels = mappedInput.mapLabels(tr(STR_DONE), "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); - return; - } - - if (state == FAILED) { - renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_AUTH_FAILED), true, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_10_FONT_ID, 320, errorMessage.c_str()); - - const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); - return; - } + const auto labels = mappedInput.mapLabels(tr(STR_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 b46d1191..577584cd 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -60,7 +60,7 @@ void KOReaderSettingsActivity::handleSelection() { // Username exitActivity(); enterNewActivity(new KeyboardEntryActivity( - renderer, mappedInput, tr(STR_KOREADER_USERNAME), KOREADER_STORE.getUsername(), 10, + renderer, mappedInput, tr(STR_KOREADER_USERNAME), KOREADER_STORE.getUsername(), 64, // maxLength false, // not password [this](const std::string& username) { @@ -77,7 +77,7 @@ void KOReaderSettingsActivity::handleSelection() { // Password exitActivity(); enterNewActivity(new KeyboardEntryActivity( - renderer, mappedInput, tr(STR_KOREADER_PASSWORD), KOREADER_STORE.getPassword(), 10, + renderer, mappedInput, tr(STR_KOREADER_PASSWORD), KOREADER_STORE.getPassword(), 64, // maxLength false, // show characters [this](const std::string& password) { @@ -96,7 +96,7 @@ void KOReaderSettingsActivity::handleSelection() { const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl; exitActivity(); enterNewActivity(new KeyboardEntryActivity( - renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, 10, + renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, 128, // maxLength - URLs can be long false, // not password [this](const std::string& url) { @@ -136,44 +136,39 @@ void KOReaderSettingsActivity::handleSelection() { void KOReaderSettingsActivity::render(Activity::RenderLock&&) { 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, tr(STR_KOREADER_SYNC), true, EpdFontFamily::BOLD); + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_KOREADER_SYNC)); - // Draw selection highlight - renderer.fillRect(0, 60 + selectedIndex * 30 - 2, pageWidth - 1, 30); + 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), [](int index) { return std::string(I18N.get(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(tr(STR_NOT_SET)) : username; + } else if (index == 1) { + return KOREADER_STORE.getPassword().empty() ? std::string(tr(STR_NOT_SET)) : std::string("******"); + } else if (index == 2) { + auto serverUrl = KOREADER_STORE.getServerUrl(); + return serverUrl.empty() ? std::string(tr(STR_DEFAULT_VALUE)) : serverUrl; + } else if (index == 3) { + return KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? std::string(tr(STR_FILENAME)) + : std::string(tr(STR_BINARY)); + } else if (index == 4) { + return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; + } + return std::string(tr(STR_NOT_SET)); + }, + true); - // 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, I18N.get(menuNames[i]), !isSelected); - - // Draw status for each item - std::string status = ""; - if (i == 0) { - status = std::string("[") + (KOREADER_STORE.getUsername().empty() ? tr(STR_NOT_SET) : tr(STR_SET)) + "]"; - } else if (i == 1) { - status = std::string("[") + (KOREADER_STORE.getPassword().empty() ? tr(STR_NOT_SET) : tr(STR_SET)) + "]"; - } else if (i == 2) { - status = - std::string("[") + (KOREADER_STORE.getServerUrl().empty() ? tr(STR_DEFAULT_VALUE) : tr(STR_CUSTOM)) + "]"; - } else if (i == 3) { - status = std::string("[") + - (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? tr(STR_FILENAME) : tr(STR_BINARY)) + - "]"; - } else if (i == 4) { - status = KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; - } - - const auto width = renderer.getTextWidth(UI_10_FONT_ID, status.c_str()); - renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status.c_str(), !isSelected); - } - - // Draw button hints - const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", ""); + 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); renderer.displayBuffer(); diff --git a/src/activities/settings/KOReaderSettingsActivity.h b/src/activities/settings/KOReaderSettingsActivity.h index 0534eabb..a4af49c9 100644 --- a/src/activities/settings/KOReaderSettingsActivity.h +++ b/src/activities/settings/KOReaderSettingsActivity.h @@ -23,7 +23,7 @@ class KOReaderSettingsActivity final : public ActivityWithSubactivity { private: ButtonNavigator buttonNavigator; - int selectedIndex = 0; + size_t selectedIndex = 0; const std::function onBack; void handleSelection(); diff --git a/src/activities/settings/OtaUpdateActivity.cpp b/src/activities/settings/OtaUpdateActivity.cpp index 8d2e7cd8..8efc3013 100644 --- a/src/activities/settings/OtaUpdateActivity.cpp +++ b/src/activities/settings/OtaUpdateActivity.cpp @@ -84,6 +84,16 @@ void OtaUpdateActivity::render(Activity::RenderLock&&) { 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}, tr(STR_UPDATE)); + const auto height = renderer.getLineHeight(UI_10_FONT_ID); + const auto top = (pageHeight - height) / 2; + float updaterProgress = 0; if (state == UPDATE_IN_PROGRESS) { LOG_DBG("OTA", "Update progress: %d / %d", updater.getProcessedSize(), updater.getTotalSize()); @@ -95,60 +105,43 @@ void OtaUpdateActivity::render(Activity::RenderLock&&) { lastUpdaterPercentage = static_cast(updaterProgress * 100); } - const auto pageWidth = renderer.getScreenWidth(); - - renderer.clearScreen(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_UPDATE), true, EpdFontFamily::BOLD); - if (state == CHECKING_FOR_UPDATE) { - renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_CHECKING_UPDATE), true, EpdFontFamily::BOLD); - renderer.displayBuffer(); - return; - } - - if (state == WAITING_CONFIRMATION) { - renderer.drawCenteredText(UI_10_FONT_ID, 200, tr(STR_NEW_UPDATE), true, EpdFontFamily::BOLD); - renderer.drawText(UI_10_FONT_ID, 20, 250, (std::string(tr(STR_CURRENT_VERSION)) + CROSSPOINT_VERSION).c_str()); - renderer.drawText(UI_10_FONT_ID, 20, 270, (std::string(tr(STR_NEW_VERSION)) + updater.getLatestVersion()).c_str()); + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_CHECKING_UPDATE)); + } else if (state == WAITING_CONFIRMATION) { + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NEW_UPDATE), true, EpdFontFamily::BOLD); + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height + metrics.verticalSpacing, + (std::string(tr(STR_CURRENT_VERSION)) + CROSSPOINT_VERSION).c_str()); + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height * 2 + metrics.verticalSpacing * 2, + (std::string(tr(STR_NEW_VERSION)) + updater.getLatestVersion()).c_str()); const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_UPDATE), "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - renderer.displayBuffer(); - return; - } + } else if (state == UPDATE_IN_PROGRESS) { + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATING)); - if (state == UPDATE_IN_PROGRESS) { - renderer.drawCenteredText(UI_10_FONT_ID, 310, tr(STR_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, + 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, 440, + UI_10_FONT_ID, y, (std::to_string(updater.getProcessedSize()) + " / " + std::to_string(updater.getTotalSize())).c_str()); - renderer.displayBuffer(); - return; + } else if (state == NO_UPDATE) { + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NO_UPDATE), true, EpdFontFamily::BOLD); + } else if (state == FAILED) { + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATE_FAILED), true, EpdFontFamily::BOLD); + } else if (state == FINISHED) { + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATE_COMPLETE), true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_10_FONT_ID, top + height + metrics.verticalSpacing, tr(STR_POWER_ON_HINT)); } - if (state == NO_UPDATE) { - renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_NO_UPDATE), true, EpdFontFamily::BOLD); - renderer.displayBuffer(); - return; - } - - if (state == FAILED) { - renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_UPDATE_FAILED), true, EpdFontFamily::BOLD); - renderer.displayBuffer(); - return; - } - - if (state == FINISHED) { - renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_UPDATE_COMPLETE), true, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_10_FONT_ID, 350, tr(STR_POWER_ON_HINT)); - renderer.displayBuffer(); - state = SHUTTING_DOWN; - return; - } + renderer.displayBuffer(); } void OtaUpdateActivity::loop() { diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 6ff173e8..680896ca 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -271,7 +271,8 @@ void SettingsActivity::render(Activity::RenderLock&&) { auto metrics = UITheme::getInstance().getMetrics(); - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_SETTINGS_TITLE)); + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_SETTINGS_TITLE), + CROSSPOINT_VERSION); std::vector tabs; tabs.reserve(categoryCount); @@ -307,12 +308,8 @@ void SettingsActivity::render(Activity::RenderLock&&) { valueText = std::to_string(SETTINGS.*(setting.valuePtr)); } return valueText; - }); - - // Draw version text - renderer.drawText(SMALL_FONT_ID, - pageWidth - metrics.versionTextRightX - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION), - metrics.versionTextY, CROSSPOINT_VERSION); + }, + true); // Draw help text const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_TOGGLE), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index 5d1a935c..be38b105 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -42,7 +42,7 @@ int KeyboardEntryActivity::getRowLength(const int row) const { case 3: return 10; // zxcvbnm,./ case 4: - return 10; // shift (2 wide), space (5 wide), backspace (2 wide), OK + return 11; // shift (2 wide), space (5 wide), backspace (2 wide), OK (2 wide) default: return 0; } @@ -191,17 +191,19 @@ void KeyboardEntryActivity::loop() { } void KeyboardEntryActivity::render(Activity::RenderLock&&) { - const auto pageWidth = renderer.getScreenWidth(); - renderer.clearScreen(); - // Draw title - renderer.drawCenteredText(UI_10_FONT_ID, startY, title.c_str()); + const auto pageWidth = renderer.getScreenWidth(); + const auto pageHeight = renderer.getScreenHeight(); + auto metrics = UITheme::getInstance().getMetrics(); + + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, title.c_str()); // Draw input field - const int inputStartY = startY + 22; - int inputEndY = startY + 22; - renderer.drawText(UI_10_FONT_ID, 10, inputStartY, "["); + const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); + const int inputStartY = + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + metrics.verticalSpacing * 4; + int inputHeight = 0; std::string displayText; if (isPassword) { @@ -216,34 +218,43 @@ void KeyboardEntryActivity::render(Activity::RenderLock&&) { // Render input text across multiple lines int lineStartIdx = 0; int lineEndIdx = displayText.length(); + int textWidth = 0; while (true) { std::string lineText = displayText.substr(lineStartIdx, lineEndIdx - lineStartIdx); - const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, lineText.c_str()); - if (textWidth <= pageWidth - 40) { - renderer.drawText(UI_10_FONT_ID, 20, inputEndY, lineText.c_str()); + textWidth = renderer.getTextWidth(UI_12_FONT_ID, lineText.c_str()); + if (textWidth <= pageWidth - 2 * metrics.contentSidePadding) { + if (metrics.keyboardCenteredText) { + renderer.drawCenteredText(UI_12_FONT_ID, inputStartY + inputHeight, lineText.c_str()); + } else { + renderer.drawText(UI_12_FONT_ID, metrics.contentSidePadding, inputStartY + inputHeight, lineText.c_str()); + } if (lineEndIdx == displayText.length()) { break; } - inputEndY += renderer.getLineHeight(UI_10_FONT_ID); + inputHeight += lineHeight; lineStartIdx = lineEndIdx; lineEndIdx = displayText.length(); } else { lineEndIdx -= 1; } } - renderer.drawText(UI_10_FONT_ID, pageWidth - 15, inputEndY, "]"); + + GUI.drawTextField(renderer, Rect{0, inputStartY, pageWidth, inputHeight}, textWidth); // Draw keyboard - use compact spacing to fit 5 rows on screen - const int keyboardStartY = inputEndY + 25; - constexpr int keyWidth = 18; - constexpr int keyHeight = 18; - constexpr int keySpacing = 3; + const int keyboardStartY = metrics.keyboardBottomAligned + ? pageHeight - metrics.buttonHintsHeight - metrics.verticalSpacing - + (metrics.keyboardKeyHeight + metrics.keyboardKeySpacing) * NUM_ROWS + : inputStartY + inputHeight + metrics.verticalSpacing * 4; + const int keyWidth = metrics.keyboardKeyWidth; + const int keyHeight = metrics.keyboardKeyHeight; + const int keySpacing = metrics.keyboardKeySpacing; const char* const* layout = shiftState ? keyboardShift : keyboard; // Calculate left margin to center the longest row (13 keys) - constexpr int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing); + const int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing); const int leftMargin = (pageWidth - maxRowWidth) / 2; for (int row = 0; row < NUM_ROWS; row++) { @@ -253,45 +264,50 @@ void KeyboardEntryActivity::render(Activity::RenderLock&&) { const int startX = leftMargin; // Handle bottom row (row 4) specially with proper multi-column keys - if (row == 4) { + if (row == SPECIAL_ROW) { // Bottom row layout: SHIFT (2 cols) | SPACE (5 cols) | <- (2 cols) | OK (2 cols) // Total: 11 visual columns, but we use logical positions for selection int currentX = startX; // SHIFT key (logical col 0, spans 2 key widths) - const bool shiftSelected = (selectedRow == 4 && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL); - static constexpr StrId shiftIds[3] = {StrId::STR_KBD_SHIFT, StrId::STR_KBD_SHIFT_CAPS, StrId::STR_KBD_LOCK}; - renderItemWithSelector(currentX + 2, rowY, I18N.get(shiftIds[shiftState]), shiftSelected); - currentX += 2 * (keyWidth + keySpacing); + const bool shiftSelected = (selectedRow == SPECIAL_ROW && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL); + const int shiftWidth = SPACE_COL - SHIFT_COL; + const int shiftXWidth = shiftWidth * (keyWidth + keySpacing); + GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, shiftXWidth, keyHeight}, shiftString[shiftState], + shiftSelected); + currentX += shiftXWidth; // Space bar (logical cols 2-6, spans 5 key widths) - const bool spaceSelected = (selectedRow == 4 && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL); - const int spaceTextWidth = renderer.getTextWidth(UI_10_FONT_ID, "_____"); - const int spaceXWidth = 5 * (keyWidth + keySpacing); - const int spaceXPos = currentX + (spaceXWidth - spaceTextWidth) / 2; - renderItemWithSelector(spaceXPos, rowY, "_____", spaceSelected); + const bool spaceSelected = + (selectedRow == SPECIAL_ROW && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL); + const int spaceWidth = BACKSPACE_COL - SPACE_COL; + const int spaceXWidth = spaceWidth * (keyWidth + keySpacing); + GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, spaceXWidth, keyHeight}, "_____", spaceSelected); currentX += spaceXWidth; // Backspace key (logical col 7, spans 2 key widths) - const bool bsSelected = (selectedRow == 4 && selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL); - renderItemWithSelector(currentX + 2, rowY, "<-", bsSelected); - currentX += 2 * (keyWidth + keySpacing); + const bool bsSelected = (selectedRow == SPECIAL_ROW && selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL); + const int backspaceWidth = DONE_COL - BACKSPACE_COL; + const int backspaceXWidth = backspaceWidth * (keyWidth + keySpacing); + GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, backspaceXWidth, keyHeight}, "<-", bsSelected); + currentX += backspaceXWidth; // OK button (logical col 9, spans 2 key widths) - const bool okSelected = (selectedRow == 4 && selectedCol >= DONE_COL); - renderItemWithSelector(currentX + 2, rowY, tr(STR_OK_BUTTON), okSelected); + const bool okSelected = (selectedRow == SPECIAL_ROW && selectedCol >= DONE_COL); + const int okWidth = getRowLength(row) - DONE_COL; + const int okXWidth = okWidth * (keyWidth + keySpacing); + GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, okXWidth, keyHeight}, tr(STR_OK_BUTTON), okSelected); } else { // Regular rows: render each key individually for (int col = 0; col < getRowLength(row); col++) { // Get the character to display const char c = layout[row][col]; std::string keyLabel(1, c); - const int charWidth = renderer.getTextWidth(UI_10_FONT_ID, keyLabel.c_str()); - const int keyX = startX + col * (keyWidth + keySpacing) + (keyWidth - charWidth) / 2; + const int keyX = startX + col * (keyWidth + keySpacing); const bool isSelected = row == selectedRow && col == selectedCol; - renderItemWithSelector(keyX, rowY, keyLabel.c_str(), isSelected); + GUI.drawKeyboardKey(renderer, Rect{keyX, rowY, keyWidth, keyHeight}, keyLabel.c_str(), isSelected); } } } @@ -301,17 +317,7 @@ void KeyboardEntryActivity::render(Activity::RenderLock&&) { GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); // Draw side button hints for Up/Down navigation - GUI.drawSideButtonHints(renderer, tr(STR_DIR_UP), tr(STR_DIR_DOWN)); + GUI.drawSideButtonHints(renderer, ">", "<"); renderer.displayBuffer(); } - -void KeyboardEntryActivity::renderItemWithSelector(const int x, const int y, const char* item, - const bool isSelected) const { - if (isSelected) { - const int itemWidth = renderer.getTextWidth(UI_10_FONT_ID, item); - renderer.drawText(UI_10_FONT_ID, x - 6, y, "["); - renderer.drawText(UI_10_FONT_ID, x + itemWidth, y, "]"); - } - renderer.drawText(UI_10_FONT_ID, x, y, item); -} diff --git a/src/activities/util/KeyboardEntryActivity.h b/src/activities/util/KeyboardEntryActivity.h index 04c9a157..c498b256 100644 --- a/src/activities/util/KeyboardEntryActivity.h +++ b/src/activities/util/KeyboardEntryActivity.h @@ -31,20 +31,18 @@ class KeyboardEntryActivity : public Activity { * @param mappedInput Reference to MappedInputManager for handling input * @param title Title to display above the keyboard * @param initialText Initial text to show in the input field - * @param startY Y position to start rendering the keyboard * @param maxLength Maximum length of input text (0 for unlimited) * @param isPassword If true, display asterisks instead of actual characters * @param onComplete Callback invoked when input is complete * @param onCancel Callback invoked when input is cancelled */ explicit KeyboardEntryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - std::string title = "Enter Text", std::string initialText = "", const int startY = 10, + std::string title = "Enter Text", std::string initialText = "", const size_t maxLength = 0, const bool isPassword = false, OnCompleteCallback onComplete = nullptr, OnCancelCallback onCancel = nullptr) : Activity("KeyboardEntry", renderer, mappedInput), title(std::move(title)), text(std::move(initialText)), - startY(startY), maxLength(maxLength), isPassword(isPassword), onComplete(std::move(onComplete)), @@ -58,7 +56,6 @@ class KeyboardEntryActivity : public Activity { private: std::string title; - int startY; std::string text; size_t maxLength; bool isPassword; @@ -91,5 +88,4 @@ class KeyboardEntryActivity : public Activity { char getSelectedChar() const; void handleKeyPress(); int getRowLength(int row) const; - void renderItemWithSelector(int x, int y, const char* item, bool isSelected) const; }; diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 1ee50b98..b808dc75 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -7,6 +7,7 @@ #include "RecentBooksStore.h" #include "components/themes/BaseTheme.h" +#include "components/themes/lyra/Lyra3CoversTheme.h" #include "components/themes/lyra/LyraTheme.h" UITheme UITheme::instance; @@ -33,6 +34,11 @@ void UITheme::setTheme(CrossPointSettings::UI_THEME type) { currentTheme = std::make_unique(); currentMetrics = &LyraMetrics::values; break; + case CrossPointSettings::UI_THEME::LYRA_3_COVERS: + LOG_DBG("UI", "Using Lyra 3 Covers theme"); + currentTheme = new Lyra3CoversTheme(); + currentMetrics = &Lyra3CoversMetrics::values; + break; } } diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 54abb232..abaf9026 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -21,6 +21,7 @@ namespace { constexpr int batteryPercentSpacing = 4; constexpr int homeMenuMargin = 20; constexpr int homeMarginTop = 30; +constexpr int subtitleY = 738; // Helper: draw battery icon at given position void drawBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, uint16_t percentage) { @@ -89,6 +90,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); + LOG_DBG("UI", "Drawing progress bar: current=%u, total=%u, percent=%d", current, total, percent); // Draw outline renderer.drawRect(rect.x, rect.y, rect.width, rect.height); @@ -187,7 +189,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; @@ -253,7 +255,12 @@ 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 { + // Hide last battery draw + constexpr int maxBatteryWidth = 80; + renderer.fillRect(rect.x + rect.width - maxBatteryWidth, rect.y + 5, maxBatteryWidth, + BaseMetrics::values.batteryHeight + 10, false); + const bool showBatteryPercentage = SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS; // Position icon at right edge, drawBatteryRight will place text to the left @@ -289,6 +296,36 @@ 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 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 - rightSpace, 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, @@ -690,3 +727,26 @@ 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()); +} + +void BaseTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const { + renderer.drawText(UI_12_FONT_ID, rect.x + 10, rect.y, "["); + renderer.drawText(UI_12_FONT_ID, rect.x + rect.width - 15, rect.y + rect.height, "]"); +} + +void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, + const bool isSelected) const { + const int itemWidth = renderer.getTextWidth(UI_10_FONT_ID, label); + const int textX = rect.x + (rect.width - itemWidth) / 2; + if (isSelected) { + renderer.drawText(UI_10_FONT_ID, textX - 6, rect.y, "["); + renderer.drawText(UI_10_FONT_ID, textX + itemWidth, rect.y, "]"); + } + renderer.drawText(UI_10_FONT_ID, textX, rect.y, label); +} \ No newline at end of file diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index 0abd1f28..07a7951c 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -51,10 +51,14 @@ struct ThemeMetrics { int buttonHintsHeight; int sideButtonHintsWidth; - int versionTextRightX; - int versionTextY; - + int progressBarHeight; int bookProgressBarHeight; + + int keyboardKeyWidth; + int keyboardKeyHeight; + int keyboardKeySpacing; + bool keyboardBottomAligned; + bool keyboardCenteredText; }; // Default theme implementation (Classic Theme) @@ -82,9 +86,13 @@ constexpr ThemeMetrics values = {.batteryWidth = 15, .homeRecentBooksCount = 1, .buttonHintsHeight = 40, .sideButtonHintsWidth = 30, - .versionTextRightX = 20, - .versionTextY = 738, - .bookProgressBarHeight = 4}; + .progressBarHeight = 16, + .bookProgressBarHeight = 4, + .keyboardKeyWidth = 18, + .keyboardKeyHeight = 18, + .keyboardKeySpacing = 3, + .keyboardBottomAligned = false, + .keyboardCenteredText = false}; } class BaseTheme { @@ -102,11 +110,14 @@ 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; - - virtual void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const; + const std::function& rowSubtitle = nullptr, + const std::function& rowIcon = nullptr, + 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 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, @@ -118,4 +129,7 @@ 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; + virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const; + virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const; }; \ 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 00000000..c02b4320 --- /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 5ad7a41a..7a5edc71 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -23,6 +23,10 @@ constexpr int batteryPercentSpacing = 4; constexpr int hPaddingInSelection = 8; constexpr int cornerRadius = 6; constexpr int topHintButtonY = 345; +constexpr int popupMarginX = 16; +constexpr int popupMarginY = 12; +constexpr int maxSubtitleWidth = 100; +constexpr int maxListValueWidth = 200; } // namespace void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { @@ -104,7 +108,7 @@ void LyraTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const b } } -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 = @@ -135,14 +139,43 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t } } + 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); + 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); 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 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 - 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); } void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector& tabs, @@ -181,7 +214,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; @@ -216,8 +249,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, @@ -231,22 +270,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 && highlightValue) { + renderer.fillRoundedRect( + contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - valueWidth, itemY, + valueWidth + hPaddingInSelection, rowHeight, cornerRadius, Color::Black); } + + renderer.drawText(UI_10_FONT_ID, rect.x + contentWidth - LyraMetrics::values.contentSidePadding - valueWidth, + itemY + 6, valueText.c_str(), !(i == selectedIndex && highlightValue)); } } } @@ -331,12 +364,10 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: const int coverHeight = LyraMetrics::values.homeCoverHeight; if (bookCount == 0) { - const int centerY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2; - renderer.drawCenteredText(UI_12_FONT_ID, centerY, tr(STR_CHOOSE_SOMETHING), true); + drawEmptyRecents(renderer, rect); return; } - // Word-wrap helper: splits text into lines fitting maxWidth, capped at maxLines with ellipsis auto wrapText = [&renderer](int fontId, const std::string& text, int maxWidth, int maxLines) -> std::vector { std::vector words; @@ -390,8 +421,6 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: return lines; }; - // Cover rendering helper: draws bitmap maintaining aspect ratio within a slot. - // Crops if wider than slot, centers if narrower. Returns actual rendered width. auto& storage = HalStorage::getInstance(); auto renderCoverBitmap = [&renderer, &storage, coverHeight](const std::string& coverBmpPath, int slotX, int slotY, int slotWidth) { @@ -418,11 +447,9 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: }; if (bookCount == 1) { - // ===== SINGLE BOOK: HORIZONTAL LAYOUT (cover left, text right) ===== const bool bookSelected = (selectorIndex == 0); const int cardX = LyraMetrics::values.contentSidePadding; const int cardWidth = rect.width - 2 * LyraMetrics::values.contentSidePadding; - // Fixed cover slot width based on typical book aspect ratio (~0.65) const int coverSlotWidth = static_cast(coverHeight * 0.65f); const int textGap = hPaddingInSelection * 2; const int textAreaX = cardX + hPaddingInSelection + coverSlotWidth + textGap; @@ -439,20 +466,14 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: coverRendered = true; } - // Selection highlight: border strips around the cover, fill the text area if (bookSelected) { - // Top strip renderer.fillRoundedRect(cardX, tileY, cardWidth, hPaddingInSelection, cornerRadius, true, true, false, false, Color::LightGray); - // Left strip (alongside cover) renderer.fillRectDither(cardX, tileY + hPaddingInSelection, hPaddingInSelection, coverHeight, Color::LightGray); - // Right strip renderer.fillRectDither(cardX + cardWidth - hPaddingInSelection, tileY + hPaddingInSelection, hPaddingInSelection, coverHeight, Color::LightGray); - // Text area background (right of cover, alongside cover height) renderer.fillRectDither(cardX + hPaddingInSelection + coverSlotWidth, tileY + hPaddingInSelection, cardWidth - hPaddingInSelection * 2 - coverSlotWidth, coverHeight, Color::LightGray); - // Bottom strip (below cover, full width) const int bottomY = tileY + hPaddingInSelection + coverHeight; const int bottomH = tileHeight - hPaddingInSelection - coverHeight; if (bottomH > 0) { @@ -461,7 +482,6 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: } } - // Title: UI_12 font, wrap generously (up to 5 lines) auto titleLines = wrapText(UI_12_FONT_ID, recentBooks[0].title, textAreaWidth, 5); const int titleLineHeight = renderer.getLineHeight(UI_12_FONT_ID); int textY = tileY + hPaddingInSelection + 3; @@ -470,7 +490,6 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: textY += titleLineHeight; } - // Author: UI_10 font if (!recentBooks[0].author.empty()) { textY += 4; auto author = renderer.truncatedText(UI_10_FONT_ID, recentBooks[0].author.c_str(), textAreaWidth); @@ -478,12 +497,9 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: } } else { - // ===== MULTI BOOK: TILE LAYOUT (2-3 books) ===== const int tileWidth = (rect.width - 2 * LyraMetrics::values.contentSidePadding) / bookCount; - // Bottom section height: everything below cover + top padding const int bottomSectionHeight = tileHeight - coverHeight - hPaddingInSelection; - // Render covers (first render only) if (!coverRendered) { for (int i = 0; i < bookCount; i++) { int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i; @@ -498,27 +514,22 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: coverRendered = true; } - // Draw selection and text for each book tile for (int i = 0; i < bookCount; i++) { bool bookSelected = (selectorIndex == i); int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i; const int maxTextWidth = tileWidth - 2 * hPaddingInSelection; if (bookSelected) { - // Top strip renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false, Color::LightGray); - // Left/right strips alongside cover renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection, coverHeight, Color::LightGray); renderer.fillRectDither(tileX + tileWidth - hPaddingInSelection, tileY + hPaddingInSelection, hPaddingInSelection, coverHeight, Color::LightGray); - // Bottom section: spans from below cover to the card bottom renderer.fillRoundedRect(tileX, tileY + coverHeight + hPaddingInSelection, tileWidth, bottomSectionHeight, cornerRadius, false, false, true, true, Color::LightGray); } - // Word-wrap title to 2 lines (UI_10) auto titleLines = wrapText(UI_10_FONT_ID, recentBooks[i].title, maxTextWidth, 2); const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID); @@ -528,7 +539,6 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: textY += lineHeight; } - // Author below title if (!recentBooks[i].author.empty()) { auto author = renderer.truncatedText(SMALL_FONT_ID, recentBooks[i].author.c_str(), maxTextWidth); renderer.drawText(SMALL_FONT_ID, tileX + hPaddingInSelection, textY + 2, author.c_str(), true); @@ -537,15 +547,22 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: } } +void LyraTheme::drawEmptyRecents(const GfxRenderer& renderer, const Rect rect) const { + constexpr int padding = 48; + renderer.drawText(UI_12_FONT_ID, rect.x + padding, + rect.y + rect.height / 2 - renderer.getLineHeight(UI_12_FONT_ID) - 2, tr(STR_NO_OPEN_BOOK), true, + EpdFontFamily::BOLD); + renderer.drawText(UI_10_FONT_ID, rect.x + padding, rect.y + rect.height / 2 + 2, tr(STR_START_READING), true); +} + void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex, 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; @@ -581,4 +598,36 @@ Rect LyraTheme::drawPopup(const GfxRenderer& renderer, const char* message) cons renderer.drawText(UI_12_FONT_ID, textX, textY, message, true, EpdFontFamily::REGULAR); renderer.displayBuffer(); return Rect{x, y, w, h}; -} \ No newline at end of file +} + +void LyraTheme::fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const { + constexpr int barHeight = 4; + + const int barWidth = layout.width - popupMarginX * 2; + const int barX = layout.x + (layout.width - barWidth) / 2; + const int barY = layout.y + layout.height - popupMarginY / 2 - barHeight / 2 - 1; + + int fillWidth = barWidth * progress / 100; + + renderer.fillRect(barX, barY, fillWidth, barHeight, false); + + renderer.displayBuffer(HalDisplay::FAST_REFRESH); +} + +void LyraTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const { + int lineY = rect.y + rect.height + renderer.getLineHeight(UI_12_FONT_ID) + LyraMetrics::values.verticalSpacing; + int lineW = textWidth + hPaddingInSelection * 2; + renderer.drawLine(rect.x + (rect.width - lineW) / 2, lineY, rect.x + (rect.width + lineW) / 2, lineY, 3); +} + +void LyraTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, + const bool isSelected) const { + if (isSelected) { + renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cornerRadius, Color::Black); + } + + const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, label); + const int textX = rect.x + (rect.width - textWidth) / 2; + const int textY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2; + renderer.drawText(UI_12_FONT_ID, textX, textY, label, !isSelected); +} diff --git a/src/components/themes/lyra/LyraTheme.h b/src/components/themes/lyra/LyraTheme.h index a881c2f9..ae48fb89 100644 --- a/src/components/themes/lyra/LyraTheme.h +++ b/src/components/themes/lyra/LyraTheme.h @@ -27,9 +27,13 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, .homeRecentBooksCount = 3, .buttonHintsHeight = 40, .sideButtonHintsWidth = 30, - .versionTextRightX = 20, - .versionTextY = 55, - .bookProgressBarHeight = 4}; + .progressBarHeight = 16, + .bookProgressBarHeight = 4, + .keyboardKeyWidth = 31, + .keyboardKeyHeight = 50, + .keyboardKeySpacing = 0, + .keyboardBottomAligned = true, + .keyboardCenteredText = true}; } class LyraTheme : public BaseTheme { @@ -38,14 +42,16 @@ class LyraTheme : public BaseTheme { // void drawProgressBar(const GfxRenderer& renderer, Rect rect, size_t current, size_t total) override; void drawBatteryLeft(const GfxRenderer& renderer, Rect rect, bool showPercentage = true) const override; void drawBatteryRight(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 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; @@ -55,5 +61,9 @@ class LyraTheme : public BaseTheme { void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector& recentBooks, const int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored, std::function storeCoverBuffer) const override; + void drawEmptyRecents(const GfxRenderer& renderer, const Rect rect) const; Rect drawPopup(const GfxRenderer& renderer, const char* message) const override; + void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const override; + void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const override; + void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const override; };