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; };