A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Lyra screens (#732)

## Summary

Implements Lyra theme for some more Crosspoint screens:

![IMG_7960
Medium](https://github.com/user-attachments/assets/5d97d91d-e5eb-4296-bbf4-917e142d9095)
![IMG_7961
Medium](https://github.com/user-attachments/assets/02d61964-2632-45ff-83c7-48b95882eb9c)
![IMG_7962
Medium](https://github.com/user-attachments/assets/cf42d20f-3a85-4669-b497-1cac4653fa5a)
![IMG_7963
Medium](https://github.com/user-attachments/assets/a8f59c37-db70-407c-a06d-3e40613a0f55)
![IMG_7964
Medium](https://github.com/user-attachments/assets/0fdaac72-077a-48f6-a8c5-1cd806a58937)
![IMG_7965
Medium](https://github.com/user-attachments/assets/5169f037-8ba8-4488-9a8a-06f5146ec1d9)


## Additional Context

- A bit of refactoring for list scrolling logic

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_

---------

Co-authored-by: Dave Allie <dave@daveallie.com>

authored by

CaptainFrito
Dave Allie
and committed by
GitHub
e7ee6ff0 c6ddc5d6

+776 -540
+13 -13
lib/GfxRenderer/GfxRenderer.cpp
··· 334 334 return; 335 335 } 336 336 337 - const int maxRadius = std::min({cornerRadius, width / 2, height / 2}); 337 + // Assume if we're not rounding all corners then we are only rounding one side 338 + const int roundedSides = (!roundTopLeft || !roundTopRight || !roundBottomLeft || !roundBottomRight) ? 1 : 2; 339 + const int maxRadius = std::min({cornerRadius, width / roundedSides, height / roundedSides}); 338 340 if (maxRadius <= 0) { 339 341 fillRectDither(x, y, width, height, color); 340 342 return; ··· 345 347 fillRectDither(x + maxRadius + 1, y, horizontalWidth - 2, height, color); 346 348 } 347 349 348 - const int verticalHeight = height - 2 * maxRadius - 2; 349 - if (verticalHeight > 0) { 350 - fillRectDither(x, y + maxRadius + 1, maxRadius + 1, verticalHeight, color); 351 - fillRectDither(x + width - maxRadius - 1, y + maxRadius + 1, maxRadius + 1, verticalHeight, color); 350 + const int leftFillTop = y + (roundTopLeft ? (maxRadius + 1) : 0); 351 + const int leftFillBottom = y + height - 1 - (roundBottomLeft ? (maxRadius + 1) : 0); 352 + if (leftFillBottom >= leftFillTop) { 353 + fillRectDither(x, leftFillTop, maxRadius + 1, leftFillBottom - leftFillTop + 1, color); 354 + } 355 + 356 + const int rightFillTop = y + (roundTopRight ? (maxRadius + 1) : 0); 357 + const int rightFillBottom = y + height - 1 - (roundBottomRight ? (maxRadius + 1) : 0); 358 + if (rightFillBottom >= rightFillTop) { 359 + fillRectDither(x + width - maxRadius - 1, rightFillTop, maxRadius + 1, rightFillBottom - rightFillTop + 1, color); 352 360 } 353 361 354 362 auto fillArcTemplated = [this](int maxRadius, int cx, int cy, int xDir, int yDir, Color color) { ··· 372 380 373 381 if (roundTopLeft) { 374 382 fillArcTemplated(maxRadius, x + maxRadius, y + maxRadius, -1, -1, color); 375 - } else { 376 - fillRectDither(x, y, maxRadius + 1, maxRadius + 1, color); 377 383 } 378 384 379 385 if (roundTopRight) { 380 386 fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + maxRadius, 1, -1, color); 381 - } else { 382 - fillRectDither(x + width - maxRadius - 1, y, maxRadius + 1, maxRadius + 1, color); 383 387 } 384 388 385 389 if (roundBottomRight) { 386 390 fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + height - maxRadius - 1, 1, 1, color); 387 - } else { 388 - fillRectDither(x + width - maxRadius - 1, y + height - maxRadius - 1, maxRadius + 1, maxRadius + 1, color); 389 391 } 390 392 391 393 if (roundBottomLeft) { 392 394 fillArcTemplated(maxRadius, x + maxRadius, y + height - maxRadius - 1, -1, 1, color); 393 - } else { 394 - fillRectDither(x, y + height - maxRadius - 1, maxRadius + 1, maxRadius + 1, color); 395 395 } 396 396 } 397 397
+1
lib/I18n/I18nKeys.h
··· 290 290 STR_UI_THEME, 291 291 STR_THEME_CLASSIC, 292 292 STR_THEME_LYRA, 293 + STR_THEME_LYRA_EXTENDED, 293 294 STR_SUNLIGHT_FADING_FIX, 294 295 STR_REMAP_FRONT_BUTTONS, 295 296 STR_OPDS_BROWSER,
+1
lib/I18n/translations/czech.yaml
··· 256 256 STR_UI_THEME: "Šablona rozhraní" 257 257 STR_THEME_CLASSIC: "Klasická" 258 258 STR_THEME_LYRA: "Lyra" 259 + STR_THEME_LYRA_EXTENDED: "Lyra Extended" 259 260 STR_SUNLIGHT_FADING_FIX: "Oprava blednutí na slunci" 260 261 STR_REMAP_FRONT_BUTTONS: "Přemapovat přední tlačítka" 261 262 STR_OPDS_BROWSER: "Prohlížeč OPDS"
+1
lib/I18n/translations/english.yaml
··· 256 256 STR_UI_THEME: "UI Theme" 257 257 STR_THEME_CLASSIC: "Classic" 258 258 STR_THEME_LYRA: "Lyra" 259 + STR_THEME_LYRA_EXTENDED: "Lyra Extended" 259 260 STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix" 260 261 STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons" 261 262 STR_OPDS_BROWSER: "OPDS Browser"
+4 -3
lib/I18n/translations/french.yaml
··· 48 48 STR_JOIN_NETWORK: "Connexion à un réseau" 49 49 STR_CREATE_HOTSPOT: "Créer un point d’accès" 50 50 STR_JOIN_DESC: "Se connecter à un réseau WiFi existant" 51 - STR_HOTSPOT_DESC: "Créer un réseau WiFi accessible depuis d’autres appareils" 51 + STR_HOTSPOT_DESC: "Créer un réseau WiFi accessible sur d’autres appareils" 52 52 STR_STARTING_HOTSPOT: "Création du point d’accès en cours…" 53 53 STR_HOTSPOT_MODE: "Mode point d’accès" 54 54 STR_CONNECT_WIFI_HINT: "Connectez un appareil à ce réseau WiFi" ··· 81 81 STR_CALIBRE_INSTRUCTION_1: "1) Installer le plugin CrossPoint Reader" 82 82 STR_CALIBRE_INSTRUCTION_2: "2) Se connecter au même réseau WiFi" 83 83 STR_CALIBRE_INSTRUCTION_3: "3) Dans Calibre : ‘Envoyer vers l’appareil’" 84 - STR_CALIBRE_INSTRUCTION_4: "“Gardez cet écran ouvert pendant le transfert”" 84 + STR_CALIBRE_INSTRUCTION_4: "4) Gardez cet écran ouvert pendant le transfert" 85 85 STR_CAT_DISPLAY: "Affichage" 86 86 STR_CAT_READER: "Lecteur" 87 87 STR_CAT_CONTROLS: "Commandes" ··· 215 215 STR_FETCH_FEED_FAILED: "Échec du téléchargement du flux" 216 216 STR_PARSE_FEED_FAILED: "Échec de l’analyse du flux" 217 217 STR_NETWORK_PREFIX: "Réseau : " 218 - STR_IP_ADDRESS_PREFIX: "Adresse IP : " 218 + STR_IP_ADDRESS_PREFIX: "IP : " 219 219 STR_SCAN_QR_WIFI_HINT: "or scan QR code with your phone to connect to Wifi." 220 220 STR_ERROR_GENERAL_FAILURE: "Erreur : Échec général" 221 221 STR_ERROR_NETWORK_NOT_FOUND: "Erreur : Réseau introuvable" ··· 256 256 STR_UI_THEME: "Thème de l’interface" 257 257 STR_THEME_CLASSIC: "Classique" 258 258 STR_THEME_LYRA: "Lyra" 259 + STR_THEME_LYRA_EXTENDED: "Lyra Extended" 259 260 STR_SUNLIGHT_FADING_FIX: "Amélioration de la lisibilité au soleil" 260 261 STR_REMAP_FRONT_BUTTONS: "Réassigner les boutons avant" 261 262 STR_OPDS_BROWSER: "Navigateur OPDS"
+1
lib/I18n/translations/german.yaml
··· 256 256 STR_UI_THEME: "System-Design" 257 257 STR_THEME_CLASSIC: "Klassisch" 258 258 STR_THEME_LYRA: "Lyra" 259 + STR_THEME_LYRA_EXTENDED: "Lyra Extended" 259 260 STR_SUNLIGHT_FADING_FIX: "Anti-Verblassen" 260 261 STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen" 261 262 STR_OPDS_BROWSER: "OPDS-Browser"
+1
lib/I18n/translations/portuguese.yaml
··· 256 256 STR_UI_THEME: "Tema da interface" 257 257 STR_THEME_CLASSIC: "Clássico" 258 258 STR_THEME_LYRA: "Lyra" 259 + STR_THEME_LYRA_EXTENDED: "Lyra Extended" 259 260 STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol" 260 261 STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais" 261 262 STR_OPDS_BROWSER: "Navegador OPDS"
+1
lib/I18n/translations/russian.yaml
··· 256 256 STR_UI_THEME: "Тема интерфейса" 257 257 STR_THEME_CLASSIC: "Классическая" 258 258 STR_THEME_LYRA: "Lyra" 259 + STR_THEME_LYRA_EXTENDED: "Lyra Extended" 259 260 STR_SUNLIGHT_FADING_FIX: "Компенсация выцветания" 260 261 STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки" 261 262 STR_OPDS_BROWSER: "OPDS браузер"
+2 -1
lib/I18n/translations/spanish.yaml
··· 255 255 STR_STATUS_BAR_FULL_CHAPTER: "Completa con progreso de capítulos" 256 256 STR_UI_THEME: "Estilo de pantalla" 257 257 STR_THEME_CLASSIC: "Clásico" 258 - STR_THEME_LYRA: "LYRA" 258 + STR_THEME_LYRA: "Lyra" 259 + STR_THEME_LYRA_EXTENDED: "Lyra Extended" 259 260 STR_SUNLIGHT_FADING_FIX: "Corrección de desvastado por sol" 260 261 STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales" 261 262 STR_OPDS_BROWSER: "Navegador opds"
+1
lib/I18n/translations/swedish.yaml
··· 256 256 STR_UI_THEME: "Användargränssnittstema" 257 257 STR_THEME_CLASSIC: "Klassisk" 258 258 STR_THEME_LYRA: "Lyra" 259 + STR_THEME_LYRA_EXTENDED: "Lyra Extended" 259 260 STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning" 260 261 STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar" 261 262 STR_OPDS_BROWSER: "OPDS-webbläsare"
+1 -1
src/CrossPointSettings.h
··· 120 120 enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT }; 121 121 122 122 // UI Theme 123 - enum UI_THEME { CLASSIC = 0, LYRA = 1 }; 123 + enum UI_THEME { CLASSIC = 0, LYRA = 1, LYRA_3_COVERS = 2 }; 124 124 125 125 // Sleep screen settings 126 126 uint8_t sleepScreen = DARK;
+2 -1
src/SettingsList.h
··· 36 36 {StrId::STR_PAGES_1, StrId::STR_PAGES_5, StrId::STR_PAGES_10, StrId::STR_PAGES_15, StrId::STR_PAGES_30}, 37 37 "refreshFrequency", StrId::STR_CAT_DISPLAY), 38 38 SettingInfo::Enum(StrId::STR_UI_THEME, &CrossPointSettings::uiTheme, 39 - {StrId::STR_THEME_CLASSIC, StrId::STR_THEME_LYRA}, "uiTheme", StrId::STR_CAT_DISPLAY), 39 + {StrId::STR_THEME_CLASSIC, StrId::STR_THEME_LYRA, StrId::STR_THEME_LYRA_EXTENDED}, "uiTheme", 40 + StrId::STR_CAT_DISPLAY), 40 41 SettingInfo::Toggle(StrId::STR_SUNLIGHT_FADING_FIX, &CrossPointSettings::fadingFix, "fadingFix", 41 42 StrId::STR_CAT_DISPLAY), 42 43
-1
src/activities/home/MyLibraryActivity.cpp
··· 167 167 } 168 168 169 169 int listSize = static_cast<int>(files.size()); 170 - 171 170 buttonNavigator.onNextRelease([this, listSize] { 172 171 selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize); 173 172 requestUpdate();
+45 -59
src/activities/network/CalibreConnectActivity.cpp
··· 169 169 } 170 170 171 171 void CalibreConnectActivity::render(Activity::RenderLock&&) { 172 - if (state == CalibreConnectState::SERVER_RUNNING) { 173 - renderer.clearScreen(); 174 - renderServerRunning(); 175 - renderer.displayBuffer(); 176 - return; 177 - } 172 + auto metrics = UITheme::getInstance().getMetrics(); 173 + const auto pageWidth = renderer.getScreenWidth(); 174 + const auto pageHeight = renderer.getScreenHeight(); 178 175 179 176 renderer.clearScreen(); 180 - const auto pageHeight = renderer.getScreenHeight(); 177 + 178 + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CALIBRE_WIRELESS)); 179 + const auto height = renderer.getLineHeight(UI_10_FONT_ID); 180 + const auto top = (pageHeight - height) / 2; 181 + 181 182 if (state == CalibreConnectState::SERVER_STARTING) { 182 - renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_CALIBRE_STARTING), true, EpdFontFamily::BOLD); 183 + renderer.drawCenteredText(UI_12_FONT_ID, top, tr(STR_CALIBRE_STARTING)); 183 184 } else if (state == CalibreConnectState::ERROR) { 184 - renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_CONNECTION_FAILED), true, EpdFontFamily::BOLD); 185 - } 186 - renderer.displayBuffer(); 187 - } 185 + renderer.drawCenteredText(UI_12_FONT_ID, top, tr(STR_CONNECTION_FAILED), true, EpdFontFamily::BOLD); 186 + } else if (state == CalibreConnectState::SERVER_RUNNING) { 187 + GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, 188 + connectedSSID.c_str(), (std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP).c_str()); 188 189 189 - void CalibreConnectActivity::renderServerRunning() const { 190 - constexpr int LINE_SPACING = 24; 191 - constexpr int SMALL_SPACING = 20; 192 - constexpr int SECTION_SPACING = 40; 193 - constexpr int TOP_PADDING = 14; 194 - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_CALIBRE_WIRELESS), true, EpdFontFamily::BOLD); 190 + int y = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 4; 191 + const auto heightText12 = renderer.getTextHeight(UI_12_FONT_ID); 192 + renderer.drawText(UI_12_FONT_ID, metrics.contentSidePadding, y, tr(STR_CALIBRE_SETUP), true, EpdFontFamily::BOLD); 193 + y += heightText12 + metrics.verticalSpacing * 2; 195 194 196 - int y = 55 + TOP_PADDING; 197 - renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_WIFI_NETWORKS), true, EpdFontFamily::BOLD); 198 - y += LINE_SPACING; 199 - std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + connectedSSID; 200 - if (ssidInfo.length() > 28) { 201 - ssidInfo.replace(25, ssidInfo.length() - 25, "..."); 202 - } 203 - renderer.drawCenteredText(UI_10_FONT_ID, y, ssidInfo.c_str()); 204 - renderer.drawCenteredText(UI_10_FONT_ID, y + LINE_SPACING, 205 - (std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP).c_str()); 195 + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y, tr(STR_CALIBRE_INSTRUCTION_1)); 196 + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height, tr(STR_CALIBRE_INSTRUCTION_2)); 197 + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height * 2, tr(STR_CALIBRE_INSTRUCTION_3)); 198 + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height * 3, tr(STR_CALIBRE_INSTRUCTION_4)); 206 199 207 - y += LINE_SPACING * 2 + SECTION_SPACING; 208 - renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_CALIBRE_SETUP), true, EpdFontFamily::BOLD); 209 - y += LINE_SPACING; 210 - renderer.drawCenteredText(SMALL_FONT_ID, y, tr(STR_CALIBRE_INSTRUCTION_1)); 211 - renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING, tr(STR_CALIBRE_INSTRUCTION_2)); 212 - renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 2, tr(STR_CALIBRE_INSTRUCTION_3)); 213 - renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 3, tr(STR_CALIBRE_INSTRUCTION_4)); 200 + y += height * 3 + metrics.verticalSpacing * 4; 201 + renderer.drawText(UI_12_FONT_ID, metrics.contentSidePadding, y, tr(STR_CALIBRE_STATUS), true, EpdFontFamily::BOLD); 202 + y += heightText12 + metrics.verticalSpacing * 2; 214 203 215 - y += SMALL_SPACING * 3 + SECTION_SPACING; 216 - renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_CALIBRE_STATUS), true, EpdFontFamily::BOLD); 217 - y += LINE_SPACING; 218 - if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) { 219 - std::string label = tr(STR_CALIBRE_RECEIVING); 220 - if (!currentUploadName.empty()) { 221 - label += ": " + currentUploadName; 222 - if (label.length() > 34) { 223 - label.replace(31, label.length() - 31, "..."); 204 + if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) { 205 + std::string label = tr(STR_CALIBRE_RECEIVING); 206 + if (!currentUploadName.empty()) { 207 + label += ": " + currentUploadName; 208 + label = renderer.truncatedText(SMALL_FONT_ID, label.c_str(), pageWidth - metrics.contentSidePadding * 2, 209 + EpdFontFamily::REGULAR); 224 210 } 211 + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y, label.c_str()); 212 + GUI.drawProgressBar(renderer, 213 + Rect{metrics.contentSidePadding, y + height + metrics.verticalSpacing, 214 + pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight}, 215 + lastProgressReceived, lastProgressTotal); 216 + y += height + metrics.verticalSpacing * 2 + metrics.progressBarHeight; 225 217 } 226 - renderer.drawCenteredText(SMALL_FONT_ID, y, label.c_str()); 227 - constexpr int barWidth = 300; 228 - constexpr int barHeight = 16; 229 - constexpr int barX = (480 - barWidth) / 2; 230 - GUI.drawProgressBar(renderer, Rect{barX, y + 22, barWidth, barHeight}, lastProgressReceived, lastProgressTotal); 231 - y += 40; 232 - } 233 218 234 - if (lastCompleteAt > 0 && (millis() - lastCompleteAt) < 6000) { 235 - std::string msg = std::string(tr(STR_CALIBRE_RECEIVED)) + lastCompleteName; 236 - if (msg.length() > 36) { 237 - msg.replace(33, msg.length() - 33, "..."); 219 + if (lastCompleteAt > 0 && (millis() - lastCompleteAt) < 6000) { 220 + std::string msg = std::string(tr(STR_CALIBRE_RECEIVED)) + lastCompleteName; 221 + msg = renderer.truncatedText(SMALL_FONT_ID, msg.c_str(), pageWidth - metrics.contentSidePadding * 2, 222 + EpdFontFamily::REGULAR); 223 + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y, msg.c_str()); 238 224 } 239 - renderer.drawCenteredText(SMALL_FONT_ID, y, msg.c_str()); 225 + 226 + const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", ""); 227 + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 240 228 } 241 - 242 - const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", ""); 243 - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 229 + renderer.displayBuffer(); 244 230 }
+63 -47
src/activities/network/CrossPointWebServerActivity.cpp
··· 24 24 constexpr const char* AP_HOSTNAME = "crosspoint"; 25 25 constexpr uint8_t AP_CHANNEL = 1; 26 26 constexpr uint8_t AP_MAX_CONNECTIONS = 4; 27 + constexpr int QR_CODE_WIDTH = 6 * 33; 28 + constexpr int QR_CODE_HEIGHT = 200; 27 29 28 30 // DNS server for captive portal (redirects all DNS queries to our IP) 29 31 DNSServer* dnsServer = nullptr; ··· 339 341 void CrossPointWebServerActivity::render(Activity::RenderLock&&) { 340 342 // Only render our own UI when server is running 341 343 // Subactivities handle their own rendering 342 - if (state == WebServerActivityState::SERVER_RUNNING) { 344 + if (state == WebServerActivityState::SERVER_RUNNING || state == WebServerActivityState::AP_STARTING) { 343 345 renderer.clearScreen(); 344 - renderServerRunning(); 345 - renderer.displayBuffer(); 346 - } else if (state == WebServerActivityState::AP_STARTING) { 347 - renderer.clearScreen(); 346 + auto metrics = UITheme::getInstance().getMetrics(); 347 + const auto pageWidth = renderer.getScreenWidth(); 348 348 const auto pageHeight = renderer.getScreenHeight(); 349 - renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_STARTING_HOTSPOT), true, EpdFontFamily::BOLD); 349 + 350 + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, 351 + isApMode ? tr(STR_HOTSPOT_MODE) : tr(STR_FILE_TRANSFER), nullptr); 352 + 353 + if (state == WebServerActivityState::SERVER_RUNNING) { 354 + GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, 355 + connectedSSID.c_str()); 356 + renderServerRunning(); 357 + } else { 358 + const auto height = renderer.getLineHeight(UI_10_FONT_ID); 359 + const auto top = (pageHeight - height) / 2; 360 + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_STARTING_HOTSPOT)); 361 + } 350 362 renderer.displayBuffer(); 351 363 } 352 364 } ··· 374 386 } 375 387 376 388 void CrossPointWebServerActivity::renderServerRunning() const { 377 - // Use consistent line spacing 378 - constexpr int LINE_SPACING = 28; // Space between lines 389 + auto metrics = UITheme::getInstance().getMetrics(); 390 + const auto pageWidth = renderer.getScreenWidth(); 379 391 380 - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_FILE_TRANSFER), true, EpdFontFamily::BOLD); 392 + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, 393 + isApMode ? tr(STR_HOTSPOT_MODE) : tr(STR_FILE_TRANSFER), nullptr); 394 + GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, 395 + connectedSSID.c_str()); 381 396 397 + int startY = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 2; 398 + int height10 = renderer.getLineHeight(UI_10_FONT_ID); 382 399 if (isApMode) { 383 - // AP mode display - center the content block 384 - int startY = 55; 400 + // AP mode display 401 + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, startY, tr(STR_CONNECT_WIFI_HINT), true, 402 + EpdFontFamily::BOLD); 403 + startY += height10 + metrics.verticalSpacing * 2; 385 404 386 - renderer.drawCenteredText(UI_10_FONT_ID, startY, tr(STR_HOTSPOT_MODE), true, EpdFontFamily::BOLD); 405 + // Show QR code for Wifi 406 + const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;"; 407 + drawQRCode(renderer, metrics.contentSidePadding, startY, wifiConfig); 387 408 388 - std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + connectedSSID; 389 - renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ssidInfo.c_str()); 409 + // Show network name 410 + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 80, 411 + connectedSSID.c_str()); 390 412 391 - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 2, tr(STR_CONNECT_WIFI_HINT)); 392 - 393 - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, tr(STR_SCAN_QR_WIFI_HINT)); 394 - // Show QR code for URL 395 - const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;"; 396 - drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 4, wifiConfig); 413 + startY += QR_CODE_HEIGHT + 2 * metrics.verticalSpacing; 397 414 398 - startY += 6 * 29 + 3 * LINE_SPACING; 399 415 // Show primary URL (hostname) 416 + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, startY, tr(STR_OPEN_URL_HINT), true, 417 + EpdFontFamily::BOLD); 418 + startY += height10 + metrics.verticalSpacing * 2; 419 + 400 420 std::string hostnameUrl = std::string("http://") + AP_HOSTNAME + ".local/"; 401 - renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str(), true, EpdFontFamily::BOLD); 421 + std::string ipUrl = tr(STR_OR_HTTP_PREFIX) + connectedIP + "/"; 422 + 423 + // Show QR code for URL 424 + drawQRCode(renderer, metrics.contentSidePadding, startY, hostnameUrl); 402 425 403 426 // Show IP address as fallback 404 - std::string ipUrl = std::string(tr(STR_OR_HTTP_PREFIX)) + connectedIP + "/"; 405 - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, ipUrl.c_str()); 406 - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, tr(STR_OPEN_URL_HINT)); 407 - 408 - // Show QR code for URL 409 - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, tr(STR_SCAN_QR_HINT)); 410 - drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 7, hostnameUrl); 427 + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 80, 428 + hostnameUrl.c_str()); 429 + renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 100, 430 + ipUrl.c_str()); 411 431 } else { 412 - // STA mode display (original behavior) 413 - const int startY = 65; 432 + startY += metrics.verticalSpacing * 2; 414 433 415 - std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + connectedSSID; 416 - if (ssidInfo.length() > 28) { 417 - ssidInfo.replace(25, ssidInfo.length() - 25, "..."); 418 - } 419 - renderer.drawCenteredText(UI_10_FONT_ID, startY, ssidInfo.c_str()); 434 + // STA mode display (original behavior) 435 + // std::string ipInfo = "IP Address: " + connectedIP; 436 + renderer.drawCenteredText(UI_10_FONT_ID, startY, tr(STR_OPEN_URL_HINT), true, EpdFontFamily::BOLD); 437 + startY += height10; 438 + renderer.drawCenteredText(UI_10_FONT_ID, startY, tr(STR_SCAN_QR_HINT), true, EpdFontFamily::BOLD); 439 + startY += height10 + metrics.verticalSpacing * 2; 420 440 421 - std::string ipInfo = std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP; 422 - renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ipInfo.c_str()); 441 + // Show QR code for URL 442 + std::string webInfo = "http://" + connectedIP + "/"; 443 + drawQRCode(renderer, (pageWidth - QR_CODE_WIDTH) / 2, startY, webInfo); 444 + startY += QR_CODE_HEIGHT + metrics.verticalSpacing * 2; 423 445 424 446 // Show web server URL prominently 425 - std::string webInfo = "http://" + connectedIP + "/"; 426 - renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 2, webInfo.c_str(), true, EpdFontFamily::BOLD); 447 + renderer.drawCenteredText(UI_10_FONT_ID, startY, webInfo.c_str(), true); 448 + startY += height10 + 5; 427 449 428 450 // Also show hostname URL 429 451 std::string hostnameUrl = std::string(tr(STR_OR_HTTP_PREFIX)) + AP_HOSTNAME + ".local/"; 430 - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str()); 431 - 432 - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, tr(STR_OPEN_URL_HINT)); 433 - 434 - // Show QR code for URL 435 - drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 6, webInfo); 436 - renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, tr(STR_SCAN_QR_HINT)); 452 + renderer.drawCenteredText(SMALL_FONT_ID, startY, hostnameUrl.c_str(), true); 437 453 } 438 454 439 455 const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", "");
+8 -23
src/activities/network/NetworkModeSelectionActivity.cpp
··· 57 57 void NetworkModeSelectionActivity::render(Activity::RenderLock&&) { 58 58 renderer.clearScreen(); 59 59 60 + auto metrics = UITheme::getInstance().getMetrics(); 60 61 const auto pageWidth = renderer.getScreenWidth(); 61 62 const auto pageHeight = renderer.getScreenHeight(); 62 63 63 - // Draw header 64 - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_FILE_TRANSFER), true, EpdFontFamily::BOLD); 65 - 66 - // Draw subtitle 67 - renderer.drawCenteredText(UI_10_FONT_ID, 50, tr(STR_HOW_CONNECT)); 64 + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_FILE_TRANSFER)); 68 65 66 + const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; 67 + const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; 69 68 // Menu items and descriptions 70 69 static constexpr StrId menuItems[MENU_ITEM_COUNT] = {StrId::STR_JOIN_NETWORK, StrId::STR_CALIBRE_WIRELESS, 71 70 StrId::STR_CREATE_HOTSPOT}; 72 71 static constexpr StrId menuDescs[MENU_ITEM_COUNT] = {StrId::STR_JOIN_DESC, StrId::STR_CALIBRE_DESC, 73 72 StrId::STR_HOTSPOT_DESC}; 74 73 75 - // Draw menu items centered on screen 76 - constexpr int itemHeight = 50; // Height for each menu item (including description) 77 - const int startY = (pageHeight - (MENU_ITEM_COUNT * itemHeight)) / 2 + 10; 78 - 79 - for (int i = 0; i < MENU_ITEM_COUNT; i++) { 80 - const int itemY = startY + i * itemHeight; 81 - const bool isSelected = (i == selectedIndex); 82 - 83 - // Draw selection highlight (black fill) for selected item 84 - if (isSelected) { 85 - renderer.fillRect(20, itemY - 2, pageWidth - 40, itemHeight - 6); 86 - } 87 - 88 - // Draw text: black=false (white text) when selected (on black background) 89 - // black=true (black text) when not selected (on white background) 90 - renderer.drawText(UI_10_FONT_ID, 30, itemY, I18N.get(menuItems[i]), /*black=*/!isSelected); 91 - renderer.drawText(SMALL_FONT_ID, 30, itemY + 22, I18N.get(menuDescs[i]), /*black=*/!isSelected); 92 - } 74 + GUI.drawList( 75 + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(MENU_ITEM_COUNT), selectedIndex, 76 + [](int index) { return std::string(I18N.get(menuItems[index])); }, 77 + [](int index) { return std::string(I18N.get(menuDescs[index])); }); 93 78 94 79 // Draw help text at bottom 95 80 const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
+34 -77
src/activities/network/WifiSelectionActivity.cpp
··· 149 149 networks.push_back(pair.second); 150 150 } 151 151 152 - // Sort by signal strength (strongest first) 153 - std::sort(networks.begin(), networks.end(), 154 - [](const WifiNetworkInfo& a, const WifiNetworkInfo& b) { return a.rssi > b.rssi; }); 155 - 156 - // Show networks with PW first 152 + // Sort: saved-password networks first, then by signal strength (strongest first) 157 153 std::sort(networks.begin(), networks.end(), [](const WifiNetworkInfo& a, const WifiNetworkInfo& b) { 158 - return a.hasSavedPassword && !b.hasSavedPassword; 154 + if (a.hasSavedPassword != b.hasSavedPassword) { 155 + return a.hasSavedPassword; 156 + } 157 + return a.rssi > b.rssi; 159 158 }); 160 159 161 160 WiFi.scanDelete(); ··· 194 193 enterNewActivity(new KeyboardEntryActivity( 195 194 renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD), 196 195 "", // No initial text 197 - 50, // Y position 198 196 64, // Max password length 199 197 false, // Show password by default (hard keyboard to use) 200 198 [this](const std::string& text) { ··· 455 453 return "||||"; // Excellent 456 454 } 457 455 if (rssi >= -60) { 458 - return "||| "; // Good 456 + return " |||"; // Good 459 457 } 460 458 if (rssi >= -70) { 461 - return "|| "; // Fair 459 + return " ||"; // Fair 462 460 } 463 - if (rssi >= -80) { 464 - return "| "; // Weak 465 - } 466 - return " "; // Very weak 461 + return " |"; // Very weak 467 462 } 468 463 469 464 void WifiSelectionActivity::render(Activity::RenderLock&&) { ··· 475 470 } 476 471 477 472 renderer.clearScreen(); 473 + 474 + auto metrics = UITheme::getInstance().getMetrics(); 475 + const auto pageWidth = renderer.getScreenWidth(); 476 + const auto pageHeight = renderer.getScreenHeight(); 477 + 478 + // Draw header 479 + char countStr[32]; 480 + snprintf(countStr, sizeof(countStr), tr(STR_NETWORKS_FOUND), networks.size()); 481 + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_WIFI_NETWORKS), 482 + countStr); 483 + GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, 484 + cachedMacAddress.c_str()); 478 485 479 486 switch (state) { 480 487 case WifiSelectionState::AUTO_CONNECTING: ··· 507 514 } 508 515 509 516 void WifiSelectionActivity::renderNetworkList() const { 517 + auto metrics = UITheme::getInstance().getMetrics(); 510 518 const auto pageWidth = renderer.getScreenWidth(); 511 519 const auto pageHeight = renderer.getScreenHeight(); 512 - 513 - // Draw header 514 - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_WIFI_NETWORKS), true, EpdFontFamily::BOLD); 515 520 516 521 if (networks.empty()) { 517 522 // No networks found or scan failed ··· 520 525 renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NO_NETWORKS)); 521 526 renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, tr(STR_PRESS_OK_SCAN)); 522 527 } else { 523 - // Calculate how many networks we can display 524 - constexpr int startY = 60; 525 - constexpr int lineHeight = 25; 526 - const int maxVisibleNetworks = (pageHeight - startY - 40) / lineHeight; 527 - 528 - // Calculate scroll offset to keep selected item visible 529 - int scrollOffset = 0; 530 - if (selectedNetworkIndex >= maxVisibleNetworks) { 531 - scrollOffset = selectedNetworkIndex - maxVisibleNetworks + 1; 532 - } 533 - 534 - // Draw networks 535 - int displayIndex = 0; 536 - for (size_t i = scrollOffset; i < networks.size() && displayIndex < maxVisibleNetworks; i++, displayIndex++) { 537 - const int networkY = startY + displayIndex * lineHeight; 538 - const auto& network = networks[i]; 539 - 540 - // Draw selection indicator 541 - if (static_cast<int>(i) == selectedNetworkIndex) { 542 - renderer.drawText(UI_10_FONT_ID, 5, networkY, ">"); 543 - } 544 - 545 - // Draw network name (truncate if too long) 546 - std::string displayName = network.ssid; 547 - if (displayName.length() > 33) { 548 - displayName.replace(30, displayName.length() - 30, "..."); 549 - } 550 - renderer.drawText(UI_10_FONT_ID, 20, networkY, displayName.c_str()); 551 - 552 - // Draw signal strength indicator 553 - std::string signalStr = getSignalStrengthIndicator(network.rssi); 554 - renderer.drawText(UI_10_FONT_ID, pageWidth - 90, networkY, signalStr.c_str()); 555 - 556 - // Draw saved indicator (checkmark) for networks with saved passwords 557 - if (network.hasSavedPassword) { 558 - renderer.drawText(UI_10_FONT_ID, pageWidth - 50, networkY, "+"); 559 - } 560 - 561 - // Draw lock icon for encrypted networks 562 - if (network.isEncrypted) { 563 - renderer.drawText(UI_10_FONT_ID, pageWidth - 30, networkY, "*"); 564 - } 565 - } 566 - 567 - // Draw scroll indicators if needed 568 - if (scrollOffset > 0) { 569 - renderer.drawText(SMALL_FONT_ID, pageWidth - 15, startY - 10, "^"); 570 - } 571 - if (scrollOffset + maxVisibleNetworks < static_cast<int>(networks.size())) { 572 - renderer.drawText(SMALL_FONT_ID, pageWidth - 15, startY + maxVisibleNetworks * lineHeight, "v"); 573 - } 574 - 575 - // Show network count 576 - char countStr[64]; 577 - snprintf(countStr, sizeof(countStr), tr(STR_NETWORKS_FOUND), networks.size()); 578 - renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 90, countStr); 528 + int contentTop = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing; 529 + int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; 530 + GUI.drawList( 531 + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(networks.size()), 532 + selectedNetworkIndex, [this](int index) { return networks[index].ssid; }, nullptr, nullptr, 533 + [this](int index) { 534 + auto network = networks[index]; 535 + return std::string(network.hasSavedPassword ? "+ " : "") + (network.isEncrypted ? "* " : "") + 536 + getSignalStrengthIndicator(network.rssi); 537 + }); 579 538 } 580 539 581 - // Show MAC address above the network count and legend 582 - renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 105, cachedMacAddress.c_str()); 583 - 584 - // Draw help text 585 - renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, tr(STR_NETWORK_LEGEND)); 540 + GUI.drawHelpText(renderer, 541 + Rect{0, pageHeight - metrics.buttonHintsHeight - metrics.contentSidePadding - 15, pageWidth, 20}, 542 + tr(STR_NETWORK_LEGEND)); 586 543 587 544 const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword; 588 545 const char* forgetLabel = hasSavedPassword ? tr(STR_FORGET_BUTTON) : "";
+1 -1
src/activities/network/WifiSelectionActivity.h
··· 45 45 ButtonNavigator buttonNavigator; 46 46 47 47 WifiSelectionState state = WifiSelectionState::SCANNING; 48 - int selectedNetworkIndex = 0; 48 + size_t selectedNetworkIndex = 0; 49 49 std::vector<WifiNetworkInfo> networks; 50 50 const std::function<void(bool connected)> onComplete; 51 51
+26 -23
src/activities/settings/ButtonRemapActivity.cpp
··· 95 95 } 96 96 97 97 void ButtonRemapActivity::render(Activity::RenderLock&&) { 98 - renderer.clearScreen(); 99 - 100 - const auto pageWidth = renderer.getScreenWidth(); 101 98 const auto labelForHardware = [&](uint8_t hardwareIndex) -> const char* { 102 99 for (uint8_t i = 0; i < kRoleCount; i++) { 103 100 if (tempMapping[i] == hardwareIndex) { ··· 107 104 return "-"; 108 105 }; 109 106 110 - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_REMAP_FRONT_BUTTONS), true, EpdFontFamily::BOLD); 111 - renderer.drawCenteredText(UI_10_FONT_ID, 40, tr(STR_REMAP_PROMPT)); 107 + auto metrics = UITheme::getInstance().getMetrics(); 108 + const auto pageWidth = renderer.getScreenWidth(); 109 + const auto pageHeight = renderer.getScreenHeight(); 112 110 113 - for (uint8_t i = 0; i < kRoleCount; i++) { 114 - const int y = 70 + i * 30; 115 - const bool isSelected = (i == currentStep); 111 + renderer.clearScreen(); 116 112 117 - // Highlight the role that is currently being assigned. 118 - if (isSelected) { 119 - renderer.fillRect(0, y - 2, pageWidth - 1, 30); 120 - } 113 + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_REMAP_FRONT_BUTTONS)); 114 + GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, 115 + tr(STR_REMAP_PROMPT)); 121 116 122 - const char* roleName = getRoleName(i); 123 - renderer.drawText(UI_10_FONT_ID, 20, y, roleName, !isSelected); 124 - 125 - // Show currently assigned hardware button (or unassigned). 126 - const char* assigned = (tempMapping[i] == kUnassigned) ? tr(STR_UNASSIGNED) : getHardwareName(tempMapping[i]); 127 - const auto width = renderer.getTextWidth(UI_10_FONT_ID, assigned); 128 - renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, y, assigned, !isSelected); 129 - } 117 + int topOffset = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing; 118 + int contentHeight = pageHeight - topOffset - metrics.buttonHintsHeight - metrics.verticalSpacing; 119 + GUI.drawList( 120 + renderer, Rect{0, topOffset, pageWidth, contentHeight}, kRoleCount, currentStep, 121 + [&](int index) { return getRoleName(static_cast<uint8_t>(index)); }, nullptr, nullptr, 122 + [&](int index) { 123 + uint8_t assignedButton = tempMapping[static_cast<uint8_t>(index)]; 124 + return (assignedButton == kUnassigned) ? tr(STR_UNASSIGNED) : getHardwareName(assignedButton); 125 + }, 126 + true); 130 127 131 128 // Temporary warning banner for duplicates. 132 129 if (!errorMessage.empty()) { 133 - renderer.drawCenteredText(UI_10_FONT_ID, 210, errorMessage.c_str(), true); 130 + GUI.drawHelpText(renderer, 131 + Rect{0, pageHeight - metrics.buttonHintsHeight - metrics.contentSidePadding - 15, pageWidth, 20}, 132 + errorMessage.c_str()); 134 133 } 135 134 136 135 // Provide side button actions at the bottom of the screen (split across two lines). 137 - renderer.drawCenteredText(SMALL_FONT_ID, 250, tr(STR_REMAP_RESET_HINT), true); 138 - renderer.drawCenteredText(SMALL_FONT_ID, 280, tr(STR_REMAP_CANCEL_HINT), true); 136 + GUI.drawHelpText(renderer, 137 + Rect{0, topOffset + 4 * metrics.listRowHeight + 4 * metrics.verticalSpacing, pageWidth, 20}, 138 + tr(STR_REMAP_RESET_HINT)); 139 + GUI.drawHelpText(renderer, 140 + Rect{0, topOffset + 4 * metrics.listRowHeight + 5 * metrics.verticalSpacing + 20, pageWidth, 20}, 141 + tr(STR_REMAP_CANCEL_HINT)); 139 142 140 143 // Live preview of logical labels under front buttons. 141 144 // This mirrors the on-device front button order: Back, Confirm, Left, Right.
+29 -35
src/activities/settings/CalibreSettingsActivity.cpp
··· 58 58 // OPDS Server URL 59 59 exitActivity(); 60 60 enterNewActivity(new KeyboardEntryActivity( 61 - renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), SETTINGS.opdsServerUrl, 10, 61 + renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), SETTINGS.opdsServerUrl, 62 62 127, // maxLength 63 63 false, // not password 64 64 [this](const std::string& url) { ··· 76 76 // Username 77 77 exitActivity(); 78 78 enterNewActivity(new KeyboardEntryActivity( 79 - renderer, mappedInput, tr(STR_USERNAME), SETTINGS.opdsUsername, 10, 79 + renderer, mappedInput, tr(STR_USERNAME), SETTINGS.opdsUsername, 80 80 63, // maxLength 81 81 false, // not password 82 82 [this](const std::string& username) { ··· 94 94 // Password 95 95 exitActivity(); 96 96 enterNewActivity(new KeyboardEntryActivity( 97 - renderer, mappedInput, tr(STR_PASSWORD), SETTINGS.opdsPassword, 10, 97 + renderer, mappedInput, tr(STR_PASSWORD), SETTINGS.opdsPassword, 98 98 63, // maxLength 99 99 false, // not password mode 100 100 [this](const std::string& password) { ··· 114 114 void CalibreSettingsActivity::render(Activity::RenderLock&&) { 115 115 renderer.clearScreen(); 116 116 117 + auto metrics = UITheme::getInstance().getMetrics(); 117 118 const auto pageWidth = renderer.getScreenWidth(); 118 - 119 - // Draw header 120 - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_OPDS_BROWSER), true, EpdFontFamily::BOLD); 121 - 122 - // Draw info text about Calibre 123 - renderer.drawCenteredText(UI_10_FONT_ID, 40, tr(STR_CALIBRE_URL_HINT)); 124 - 125 - // Draw selection highlight 126 - renderer.fillRect(0, 70 + selectedIndex * 30 - 2, pageWidth - 1, 30); 127 - 128 - // Draw menu items 129 - for (int i = 0; i < MENU_ITEMS; i++) { 130 - const int settingY = 70 + i * 30; 131 - const bool isSelected = (i == selectedIndex); 132 - 133 - renderer.drawText(UI_10_FONT_ID, 20, settingY, I18N.get(menuNames[i]), !isSelected); 119 + const auto pageHeight = renderer.getScreenHeight(); 120 + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_OPDS_BROWSER)); 121 + GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight}, 122 + tr(STR_CALIBRE_URL_HINT)); 134 123 135 - // Draw status for each setting 136 - std::string status = std::string("[") + tr(STR_NOT_SET) + "]"; 137 - if (i == 0) { 138 - status = (strlen(SETTINGS.opdsServerUrl) > 0) ? std::string("[") + tr(STR_SET) + "]" 139 - : std::string("[") + tr(STR_NOT_SET) + "]"; 140 - } else if (i == 1) { 141 - status = (strlen(SETTINGS.opdsUsername) > 0) ? std::string("[") + tr(STR_SET) + "]" 142 - : std::string("[") + tr(STR_NOT_SET) + "]"; 143 - } else if (i == 2) { 144 - status = (strlen(SETTINGS.opdsPassword) > 0) ? std::string("[") + tr(STR_SET) + "]" 145 - : std::string("[") + tr(STR_NOT_SET) + "]"; 146 - } 147 - const auto width = renderer.getTextWidth(UI_10_FONT_ID, status.c_str()); 148 - renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status.c_str(), !isSelected); 149 - } 124 + const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + metrics.tabBarHeight; 125 + const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; 126 + GUI.drawList( 127 + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(MENU_ITEMS), 128 + static_cast<int>(selectedIndex), [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr, 129 + nullptr, 130 + [this](int index) { 131 + // Draw status for each setting 132 + if (index == 0) { 133 + return (strlen(SETTINGS.opdsServerUrl) > 0) ? std::string(SETTINGS.opdsServerUrl) 134 + : std::string(tr(STR_NOT_SET)); 135 + } else if (index == 1) { 136 + return (strlen(SETTINGS.opdsUsername) > 0) ? std::string(SETTINGS.opdsUsername) 137 + : std::string(tr(STR_NOT_SET)); 138 + } else if (index == 2) { 139 + return (strlen(SETTINGS.opdsPassword) > 0) ? std::string("******") : std::string(tr(STR_NOT_SET)); 140 + } 141 + return std::string(tr(STR_NOT_SET)); 142 + }, 143 + true); 150 144 151 - // Draw button hints 145 + // Draw help text at bottom 152 146 const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); 153 147 GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 154 148
+1 -1
src/activities/settings/CalibreSettingsActivity.h
··· 23 23 private: 24 24 ButtonNavigator buttonNavigator; 25 25 26 - int selectedIndex = 0; 26 + size_t selectedIndex = 0; 27 27 const std::function<void()> onBack; 28 28 void handleSelection(); 29 29 };
+5 -2
src/activities/settings/ClearCacheActivity.cpp
··· 19 19 void ClearCacheActivity::onExit() { ActivityWithSubactivity::onExit(); } 20 20 21 21 void ClearCacheActivity::render(Activity::RenderLock&&) { 22 + auto metrics = UITheme::getInstance().getMetrics(); 23 + const auto pageWidth = renderer.getScreenWidth(); 22 24 const auto pageHeight = renderer.getScreenHeight(); 23 25 24 26 renderer.clearScreen(); 25 - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_CLEAR_READING_CACHE), true, EpdFontFamily::BOLD); 27 + 28 + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CLEAR_READING_CACHE)); 26 29 27 30 if (state == WARNING) { 28 31 renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, tr(STR_CLEAR_CACHE_WARNING_1), true); ··· 38 41 } 39 42 40 43 if (state == CLEARING) { 41 - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_CLEARING_CACHE), true, EpdFontFamily::BOLD); 44 + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_CLEARING_CACHE)); 42 45 renderer.displayBuffer(); 43 46 return; 44 47 }
+17 -22
src/activities/settings/KOReaderAuthActivity.cpp
··· 90 90 91 91 void KOReaderAuthActivity::render(Activity::RenderLock&&) { 92 92 renderer.clearScreen(); 93 - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_KOREADER_AUTH), true, EpdFontFamily::BOLD); 94 93 95 - if (state == AUTHENTICATING) { 96 - renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD); 97 - renderer.displayBuffer(); 98 - return; 99 - } 94 + auto metrics = UITheme::getInstance().getMetrics(); 95 + const auto pageWidth = renderer.getScreenWidth(); 96 + const auto pageHeight = renderer.getScreenHeight(); 100 97 101 - if (state == SUCCESS) { 102 - renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_AUTH_SUCCESS), true, EpdFontFamily::BOLD); 103 - renderer.drawCenteredText(UI_10_FONT_ID, 320, tr(STR_SYNC_READY)); 98 + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_KOREADER_AUTH)); 99 + const auto height = renderer.getLineHeight(UI_10_FONT_ID); 100 + const auto top = (pageHeight - height) / 2; 104 101 105 - const auto labels = mappedInput.mapLabels(tr(STR_DONE), "", "", ""); 106 - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 107 - renderer.displayBuffer(); 108 - return; 102 + if (state == AUTHENTICATING) { 103 + renderer.drawCenteredText(UI_10_FONT_ID, top, statusMessage.c_str()); 104 + } else if (state == SUCCESS) { 105 + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_SUCCESS), true, EpdFontFamily::BOLD); 106 + renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, tr(STR_SYNC_READY)); 107 + } else if (state == FAILED) { 108 + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_FAILED), true, EpdFontFamily::BOLD); 109 + renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, errorMessage.c_str()); 109 110 } 110 111 111 - if (state == FAILED) { 112 - renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_AUTH_FAILED), true, EpdFontFamily::BOLD); 113 - renderer.drawCenteredText(UI_10_FONT_ID, 320, errorMessage.c_str()); 114 - 115 - const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); 116 - GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 117 - renderer.displayBuffer(); 118 - return; 119 - } 112 + const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); 113 + GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 114 + renderer.displayBuffer(); 120 115 } 121 116 122 117 void KOReaderAuthActivity::loop() {
+32 -36
src/activities/settings/KOReaderSettingsActivity.cpp
··· 60 60 // Username 61 61 exitActivity(); 62 62 enterNewActivity(new KeyboardEntryActivity( 63 - renderer, mappedInput, tr(STR_KOREADER_USERNAME), KOREADER_STORE.getUsername(), 10, 63 + renderer, mappedInput, tr(STR_KOREADER_USERNAME), KOREADER_STORE.getUsername(), 64 64 64, // maxLength 65 65 false, // not password 66 66 [this](const std::string& username) { ··· 77 77 // Password 78 78 exitActivity(); 79 79 enterNewActivity(new KeyboardEntryActivity( 80 - renderer, mappedInput, tr(STR_KOREADER_PASSWORD), KOREADER_STORE.getPassword(), 10, 80 + renderer, mappedInput, tr(STR_KOREADER_PASSWORD), KOREADER_STORE.getPassword(), 81 81 64, // maxLength 82 82 false, // show characters 83 83 [this](const std::string& password) { ··· 96 96 const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl; 97 97 exitActivity(); 98 98 enterNewActivity(new KeyboardEntryActivity( 99 - renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, 10, 99 + renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, 100 100 128, // maxLength - URLs can be long 101 101 false, // not password 102 102 [this](const std::string& url) { ··· 136 136 void KOReaderSettingsActivity::render(Activity::RenderLock&&) { 137 137 renderer.clearScreen(); 138 138 139 + auto metrics = UITheme::getInstance().getMetrics(); 139 140 const auto pageWidth = renderer.getScreenWidth(); 140 - 141 - // Draw header 142 - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_KOREADER_SYNC), true, EpdFontFamily::BOLD); 143 - 144 - // Draw selection highlight 145 - renderer.fillRect(0, 60 + selectedIndex * 30 - 2, pageWidth - 1, 30); 146 - 147 - // Draw menu items 148 - for (int i = 0; i < MENU_ITEMS; i++) { 149 - const int settingY = 60 + i * 30; 150 - const bool isSelected = (i == selectedIndex); 141 + const auto pageHeight = renderer.getScreenHeight(); 151 142 152 - renderer.drawText(UI_10_FONT_ID, 20, settingY, I18N.get(menuNames[i]), !isSelected); 153 - 154 - // Draw status for each item 155 - std::string status = ""; 156 - if (i == 0) { 157 - status = std::string("[") + (KOREADER_STORE.getUsername().empty() ? tr(STR_NOT_SET) : tr(STR_SET)) + "]"; 158 - } else if (i == 1) { 159 - status = std::string("[") + (KOREADER_STORE.getPassword().empty() ? tr(STR_NOT_SET) : tr(STR_SET)) + "]"; 160 - } else if (i == 2) { 161 - status = 162 - std::string("[") + (KOREADER_STORE.getServerUrl().empty() ? tr(STR_DEFAULT_VALUE) : tr(STR_CUSTOM)) + "]"; 163 - } else if (i == 3) { 164 - status = std::string("[") + 165 - (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? tr(STR_FILENAME) : tr(STR_BINARY)) + 166 - "]"; 167 - } else if (i == 4) { 168 - status = KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; 169 - } 143 + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_KOREADER_SYNC)); 170 144 171 - const auto width = renderer.getTextWidth(UI_10_FONT_ID, status.c_str()); 172 - renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status.c_str(), !isSelected); 173 - } 145 + const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; 146 + const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; 147 + GUI.drawList( 148 + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(MENU_ITEMS), 149 + static_cast<int>(selectedIndex), [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr, 150 + nullptr, 151 + [this](int index) { 152 + // Draw status for each setting 153 + if (index == 0) { 154 + auto username = KOREADER_STORE.getUsername(); 155 + return username.empty() ? std::string(tr(STR_NOT_SET)) : username; 156 + } else if (index == 1) { 157 + return KOREADER_STORE.getPassword().empty() ? std::string(tr(STR_NOT_SET)) : std::string("******"); 158 + } else if (index == 2) { 159 + auto serverUrl = KOREADER_STORE.getServerUrl(); 160 + return serverUrl.empty() ? std::string(tr(STR_DEFAULT_VALUE)) : serverUrl; 161 + } else if (index == 3) { 162 + return KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? std::string(tr(STR_FILENAME)) 163 + : std::string(tr(STR_BINARY)); 164 + } else if (index == 4) { 165 + return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]"; 166 + } 167 + return std::string(tr(STR_NOT_SET)); 168 + }, 169 + true); 174 170 175 - // Draw button hints 171 + // Draw help text at bottom 176 172 const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); 177 173 GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 178 174
+1 -1
src/activities/settings/KOReaderSettingsActivity.h
··· 23 23 private: 24 24 ButtonNavigator buttonNavigator; 25 25 26 - int selectedIndex = 0; 26 + size_t selectedIndex = 0; 27 27 const std::function<void()> onBack; 28 28 29 29 void handleSelection();
+37 -44
src/activities/settings/OtaUpdateActivity.cpp
··· 84 84 return; 85 85 } 86 86 87 + auto metrics = UITheme::getInstance().getMetrics(); 88 + const auto pageWidth = renderer.getScreenWidth(); 89 + const auto pageHeight = renderer.getScreenHeight(); 90 + 91 + renderer.clearScreen(); 92 + 93 + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_UPDATE)); 94 + const auto height = renderer.getLineHeight(UI_10_FONT_ID); 95 + const auto top = (pageHeight - height) / 2; 96 + 87 97 float updaterProgress = 0; 88 98 if (state == UPDATE_IN_PROGRESS) { 89 99 LOG_DBG("OTA", "Update progress: %d / %d", updater.getProcessedSize(), updater.getTotalSize()); ··· 95 105 lastUpdaterPercentage = static_cast<int>(updaterProgress * 100); 96 106 } 97 107 98 - const auto pageWidth = renderer.getScreenWidth(); 99 - 100 - renderer.clearScreen(); 101 - renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_UPDATE), true, EpdFontFamily::BOLD); 102 - 103 108 if (state == CHECKING_FOR_UPDATE) { 104 - renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_CHECKING_UPDATE), true, EpdFontFamily::BOLD); 105 - renderer.displayBuffer(); 106 - return; 107 - } 108 - 109 - if (state == WAITING_CONFIRMATION) { 110 - renderer.drawCenteredText(UI_10_FONT_ID, 200, tr(STR_NEW_UPDATE), true, EpdFontFamily::BOLD); 111 - renderer.drawText(UI_10_FONT_ID, 20, 250, (std::string(tr(STR_CURRENT_VERSION)) + CROSSPOINT_VERSION).c_str()); 112 - renderer.drawText(UI_10_FONT_ID, 20, 270, (std::string(tr(STR_NEW_VERSION)) + updater.getLatestVersion()).c_str()); 109 + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_CHECKING_UPDATE)); 110 + } else if (state == WAITING_CONFIRMATION) { 111 + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NEW_UPDATE), true, EpdFontFamily::BOLD); 112 + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height + metrics.verticalSpacing, 113 + (std::string(tr(STR_CURRENT_VERSION)) + CROSSPOINT_VERSION).c_str()); 114 + renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height * 2 + metrics.verticalSpacing * 2, 115 + (std::string(tr(STR_NEW_VERSION)) + updater.getLatestVersion()).c_str()); 113 116 114 117 const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_UPDATE), "", ""); 115 118 GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 116 - renderer.displayBuffer(); 117 - return; 118 - } 119 + } else if (state == UPDATE_IN_PROGRESS) { 120 + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATING)); 121 + 122 + int y = top + height + metrics.verticalSpacing; 123 + GUI.drawProgressBar( 124 + renderer, 125 + Rect{metrics.contentSidePadding, y, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight}, 126 + static_cast<int>(updaterProgress * 100), 100); 119 127 120 - if (state == UPDATE_IN_PROGRESS) { 121 - renderer.drawCenteredText(UI_10_FONT_ID, 310, tr(STR_UPDATING), true, EpdFontFamily::BOLD); 122 - renderer.drawRect(20, 350, pageWidth - 40, 50); 123 - renderer.fillRect(24, 354, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44)), 42); 124 - renderer.drawCenteredText(UI_10_FONT_ID, 420, 128 + y += metrics.progressBarHeight + metrics.verticalSpacing; 129 + renderer.drawCenteredText(UI_10_FONT_ID, y, 125 130 (std::to_string(static_cast<int>(updaterProgress * 100)) + "%").c_str()); 131 + y += height + metrics.verticalSpacing; 126 132 renderer.drawCenteredText( 127 - UI_10_FONT_ID, 440, 133 + UI_10_FONT_ID, y, 128 134 (std::to_string(updater.getProcessedSize()) + " / " + std::to_string(updater.getTotalSize())).c_str()); 129 - renderer.displayBuffer(); 130 - return; 131 - } 132 - 133 - if (state == NO_UPDATE) { 134 - renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_NO_UPDATE), true, EpdFontFamily::BOLD); 135 - renderer.displayBuffer(); 136 - return; 137 - } 138 - 139 - if (state == FAILED) { 140 - renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_UPDATE_FAILED), true, EpdFontFamily::BOLD); 141 - renderer.displayBuffer(); 142 - return; 135 + } else if (state == NO_UPDATE) { 136 + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NO_UPDATE), true, EpdFontFamily::BOLD); 137 + } else if (state == FAILED) { 138 + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATE_FAILED), true, EpdFontFamily::BOLD); 139 + } else if (state == FINISHED) { 140 + renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATE_COMPLETE), true, EpdFontFamily::BOLD); 141 + renderer.drawCenteredText(UI_10_FONT_ID, top + height + metrics.verticalSpacing, tr(STR_POWER_ON_HINT)); 143 142 } 144 143 145 - if (state == FINISHED) { 146 - renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_UPDATE_COMPLETE), true, EpdFontFamily::BOLD); 147 - renderer.drawCenteredText(UI_10_FONT_ID, 350, tr(STR_POWER_ON_HINT)); 148 - renderer.displayBuffer(); 149 - state = SHUTTING_DOWN; 150 - return; 151 - } 144 + renderer.displayBuffer(); 152 145 } 153 146 154 147 void OtaUpdateActivity::loop() {
+4 -7
src/activities/settings/SettingsActivity.cpp
··· 218 218 219 219 auto metrics = UITheme::getInstance().getMetrics(); 220 220 221 - GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_SETTINGS_TITLE)); 221 + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_SETTINGS_TITLE), 222 + CROSSPOINT_VERSION); 222 223 223 224 std::vector<TabInfo> tabs; 224 225 tabs.reserve(categoryCount); ··· 249 250 valueText = std::to_string(SETTINGS.*(setting.valuePtr)); 250 251 } 251 252 return valueText; 252 - }); 253 - 254 - // Draw version text 255 - renderer.drawText(SMALL_FONT_ID, 256 - pageWidth - metrics.versionTextRightX - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION), 257 - metrics.versionTextY, CROSSPOINT_VERSION); 253 + }, 254 + true); 258 255 259 256 // Draw help text 260 257 const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_TOGGLE), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
+53 -47
src/activities/util/KeyboardEntryActivity.cpp
··· 42 42 case 3: 43 43 return 10; // zxcvbnm,./ 44 44 case 4: 45 - return 10; // shift (2 wide), space (5 wide), backspace (2 wide), OK 45 + return 11; // shift (2 wide), space (5 wide), backspace (2 wide), OK (2 wide) 46 46 default: 47 47 return 0; 48 48 } ··· 191 191 } 192 192 193 193 void KeyboardEntryActivity::render(Activity::RenderLock&&) { 194 + renderer.clearScreen(); 195 + 194 196 const auto pageWidth = renderer.getScreenWidth(); 197 + const auto pageHeight = renderer.getScreenHeight(); 198 + auto metrics = UITheme::getInstance().getMetrics(); 195 199 196 - renderer.clearScreen(); 197 - 198 - // Draw title 199 - renderer.drawCenteredText(UI_10_FONT_ID, startY, title.c_str()); 200 + GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, title.c_str()); 200 201 201 202 // Draw input field 202 - const int inputStartY = startY + 22; 203 - int inputEndY = startY + 22; 204 - renderer.drawText(UI_10_FONT_ID, 10, inputStartY, "["); 203 + const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); 204 + const int inputStartY = 205 + metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + metrics.verticalSpacing * 4; 206 + int inputHeight = 0; 205 207 206 208 std::string displayText; 207 209 if (isPassword) { ··· 216 218 // Render input text across multiple lines 217 219 int lineStartIdx = 0; 218 220 int lineEndIdx = displayText.length(); 221 + int textWidth = 0; 219 222 while (true) { 220 223 std::string lineText = displayText.substr(lineStartIdx, lineEndIdx - lineStartIdx); 221 - const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, lineText.c_str()); 222 - if (textWidth <= pageWidth - 40) { 223 - renderer.drawText(UI_10_FONT_ID, 20, inputEndY, lineText.c_str()); 224 + textWidth = renderer.getTextWidth(UI_12_FONT_ID, lineText.c_str()); 225 + if (textWidth <= pageWidth - 2 * metrics.contentSidePadding) { 226 + if (metrics.keyboardCenteredText) { 227 + renderer.drawCenteredText(UI_12_FONT_ID, inputStartY + inputHeight, lineText.c_str()); 228 + } else { 229 + renderer.drawText(UI_12_FONT_ID, metrics.contentSidePadding, inputStartY + inputHeight, lineText.c_str()); 230 + } 224 231 if (lineEndIdx == displayText.length()) { 225 232 break; 226 233 } 227 234 228 - inputEndY += renderer.getLineHeight(UI_10_FONT_ID); 235 + inputHeight += lineHeight; 229 236 lineStartIdx = lineEndIdx; 230 237 lineEndIdx = displayText.length(); 231 238 } else { 232 239 lineEndIdx -= 1; 233 240 } 234 241 } 235 - renderer.drawText(UI_10_FONT_ID, pageWidth - 15, inputEndY, "]"); 242 + 243 + GUI.drawTextField(renderer, Rect{0, inputStartY, pageWidth, inputHeight}, textWidth); 236 244 237 245 // Draw keyboard - use compact spacing to fit 5 rows on screen 238 - const int keyboardStartY = inputEndY + 25; 239 - constexpr int keyWidth = 18; 240 - constexpr int keyHeight = 18; 241 - constexpr int keySpacing = 3; 246 + const int keyboardStartY = metrics.keyboardBottomAligned 247 + ? pageHeight - metrics.buttonHintsHeight - metrics.verticalSpacing - 248 + (metrics.keyboardKeyHeight + metrics.keyboardKeySpacing) * NUM_ROWS 249 + : inputStartY + inputHeight + metrics.verticalSpacing * 4; 250 + const int keyWidth = metrics.keyboardKeyWidth; 251 + const int keyHeight = metrics.keyboardKeyHeight; 252 + const int keySpacing = metrics.keyboardKeySpacing; 242 253 243 254 const char* const* layout = shiftState ? keyboardShift : keyboard; 244 255 245 256 // Calculate left margin to center the longest row (13 keys) 246 - constexpr int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing); 257 + const int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing); 247 258 const int leftMargin = (pageWidth - maxRowWidth) / 2; 248 259 249 260 for (int row = 0; row < NUM_ROWS; row++) { ··· 253 264 const int startX = leftMargin; 254 265 255 266 // Handle bottom row (row 4) specially with proper multi-column keys 256 - if (row == 4) { 267 + if (row == SPECIAL_ROW) { 257 268 // Bottom row layout: SHIFT (2 cols) | SPACE (5 cols) | <- (2 cols) | OK (2 cols) 258 269 // Total: 11 visual columns, but we use logical positions for selection 259 270 260 271 int currentX = startX; 261 272 262 273 // SHIFT key (logical col 0, spans 2 key widths) 263 - const bool shiftSelected = (selectedRow == 4 && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL); 264 - static constexpr StrId shiftIds[3] = {StrId::STR_KBD_SHIFT, StrId::STR_KBD_SHIFT_CAPS, StrId::STR_KBD_LOCK}; 265 - renderItemWithSelector(currentX + 2, rowY, I18N.get(shiftIds[shiftState]), shiftSelected); 266 - currentX += 2 * (keyWidth + keySpacing); 274 + const bool shiftSelected = (selectedRow == SPECIAL_ROW && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL); 275 + const int shiftWidth = SPACE_COL - SHIFT_COL; 276 + const int shiftXWidth = shiftWidth * (keyWidth + keySpacing); 277 + GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, shiftXWidth, keyHeight}, shiftString[shiftState], 278 + shiftSelected); 279 + currentX += shiftXWidth; 267 280 268 281 // Space bar (logical cols 2-6, spans 5 key widths) 269 - const bool spaceSelected = (selectedRow == 4 && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL); 270 - const int spaceTextWidth = renderer.getTextWidth(UI_10_FONT_ID, "_____"); 271 - const int spaceXWidth = 5 * (keyWidth + keySpacing); 272 - const int spaceXPos = currentX + (spaceXWidth - spaceTextWidth) / 2; 273 - renderItemWithSelector(spaceXPos, rowY, "_____", spaceSelected); 282 + const bool spaceSelected = 283 + (selectedRow == SPECIAL_ROW && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL); 284 + const int spaceWidth = BACKSPACE_COL - SPACE_COL; 285 + const int spaceXWidth = spaceWidth * (keyWidth + keySpacing); 286 + GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, spaceXWidth, keyHeight}, "_____", spaceSelected); 274 287 currentX += spaceXWidth; 275 288 276 289 // Backspace key (logical col 7, spans 2 key widths) 277 - const bool bsSelected = (selectedRow == 4 && selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL); 278 - renderItemWithSelector(currentX + 2, rowY, "<-", bsSelected); 279 - currentX += 2 * (keyWidth + keySpacing); 290 + const bool bsSelected = (selectedRow == SPECIAL_ROW && selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL); 291 + const int backspaceWidth = DONE_COL - BACKSPACE_COL; 292 + const int backspaceXWidth = backspaceWidth * (keyWidth + keySpacing); 293 + GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, backspaceXWidth, keyHeight}, "<-", bsSelected); 294 + currentX += backspaceXWidth; 280 295 281 296 // OK button (logical col 9, spans 2 key widths) 282 - const bool okSelected = (selectedRow == 4 && selectedCol >= DONE_COL); 283 - renderItemWithSelector(currentX + 2, rowY, tr(STR_OK_BUTTON), okSelected); 297 + const bool okSelected = (selectedRow == SPECIAL_ROW && selectedCol >= DONE_COL); 298 + const int okWidth = getRowLength(row) - DONE_COL; 299 + const int okXWidth = okWidth * (keyWidth + keySpacing); 300 + GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, okXWidth, keyHeight}, tr(STR_OK_BUTTON), okSelected); 284 301 } else { 285 302 // Regular rows: render each key individually 286 303 for (int col = 0; col < getRowLength(row); col++) { 287 304 // Get the character to display 288 305 const char c = layout[row][col]; 289 306 std::string keyLabel(1, c); 290 - const int charWidth = renderer.getTextWidth(UI_10_FONT_ID, keyLabel.c_str()); 291 307 292 - const int keyX = startX + col * (keyWidth + keySpacing) + (keyWidth - charWidth) / 2; 308 + const int keyX = startX + col * (keyWidth + keySpacing); 293 309 const bool isSelected = row == selectedRow && col == selectedCol; 294 - renderItemWithSelector(keyX, rowY, keyLabel.c_str(), isSelected); 310 + GUI.drawKeyboardKey(renderer, Rect{keyX, rowY, keyWidth, keyHeight}, keyLabel.c_str(), isSelected); 295 311 } 296 312 } 297 313 } ··· 301 317 GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 302 318 303 319 // Draw side button hints for Up/Down navigation 304 - GUI.drawSideButtonHints(renderer, tr(STR_DIR_UP), tr(STR_DIR_DOWN)); 320 + GUI.drawSideButtonHints(renderer, ">", "<"); 305 321 306 322 renderer.displayBuffer(); 307 323 } 308 - 309 - void KeyboardEntryActivity::renderItemWithSelector(const int x, const int y, const char* item, 310 - const bool isSelected) const { 311 - if (isSelected) { 312 - const int itemWidth = renderer.getTextWidth(UI_10_FONT_ID, item); 313 - renderer.drawText(UI_10_FONT_ID, x - 6, y, "["); 314 - renderer.drawText(UI_10_FONT_ID, x + itemWidth, y, "]"); 315 - } 316 - renderer.drawText(UI_10_FONT_ID, x, y, item); 317 - }
+1 -5
src/activities/util/KeyboardEntryActivity.h
··· 31 31 * @param mappedInput Reference to MappedInputManager for handling input 32 32 * @param title Title to display above the keyboard 33 33 * @param initialText Initial text to show in the input field 34 - * @param startY Y position to start rendering the keyboard 35 34 * @param maxLength Maximum length of input text (0 for unlimited) 36 35 * @param isPassword If true, display asterisks instead of actual characters 37 36 * @param onComplete Callback invoked when input is complete 38 37 * @param onCancel Callback invoked when input is cancelled 39 38 */ 40 39 explicit KeyboardEntryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 41 - std::string title = "Enter Text", std::string initialText = "", const int startY = 10, 40 + std::string title = "Enter Text", std::string initialText = "", 42 41 const size_t maxLength = 0, const bool isPassword = false, 43 42 OnCompleteCallback onComplete = nullptr, OnCancelCallback onCancel = nullptr) 44 43 : Activity("KeyboardEntry", renderer, mappedInput), 45 44 title(std::move(title)), 46 45 text(std::move(initialText)), 47 - startY(startY), 48 46 maxLength(maxLength), 49 47 isPassword(isPassword), 50 48 onComplete(std::move(onComplete)), ··· 58 56 59 57 private: 60 58 std::string title; 61 - int startY; 62 59 std::string text; 63 60 size_t maxLength; 64 61 bool isPassword; ··· 91 88 char getSelectedChar() const; 92 89 void handleKeyPress(); 93 90 int getRowLength(int row) const; 94 - void renderItemWithSelector(int x, int y, const char* item, bool isSelected) const; 95 91 };
+6
src/components/UITheme.cpp
··· 7 7 8 8 #include "RecentBooksStore.h" 9 9 #include "components/themes/BaseTheme.h" 10 + #include "components/themes/lyra/Lyra3CoversTheme.h" 10 11 #include "components/themes/lyra/LyraTheme.h" 11 12 12 13 UITheme UITheme::instance; ··· 32 33 LOG_DBG("UI", "Using Lyra theme"); 33 34 currentTheme = new LyraTheme(); 34 35 currentMetrics = &LyraMetrics::values; 36 + break; 37 + case CrossPointSettings::UI_THEME::LYRA_3_COVERS: 38 + LOG_DBG("UI", "Using Lyra 3 Covers theme"); 39 + currentTheme = new Lyra3CoversTheme(); 40 + currentMetrics = &Lyra3CoversMetrics::values; 35 41 break; 36 42 } 37 43 }
-1
src/components/UITheme.h
··· 1 1 #pragma once 2 2 3 3 #include <functional> 4 - #include <vector> 5 4 6 5 #include "CrossPointSettings.h" 7 6 #include "components/themes/BaseTheme.h"
+62 -2
src/components/themes/BaseTheme.cpp
··· 19 19 constexpr int batteryPercentSpacing = 4; 20 20 constexpr int homeMenuMargin = 20; 21 21 constexpr int homeMarginTop = 30; 22 + constexpr int subtitleY = 738; 22 23 23 24 // Helper: draw battery icon at given position 24 25 void drawBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, uint16_t percentage) { ··· 87 88 // Use 64-bit arithmetic to avoid overflow for large files 88 89 const int percent = static_cast<int>((static_cast<uint64_t>(current) * 100) / total); 89 90 91 + LOG_DBG("UI", "Drawing progress bar: current=%u, total=%u, percent=%d", current, total, percent); 90 92 // Draw outline 91 93 renderer.drawRect(rect.x, rect.y, rect.width, rect.height); 92 94 ··· 185 187 const std::function<std::string(int index)>& rowTitle, 186 188 const std::function<std::string(int index)>& rowSubtitle, 187 189 const std::function<std::string(int index)>& rowIcon, 188 - const std::function<std::string(int index)>& rowValue) const { 190 + const std::function<std::string(int index)>& rowValue, bool highlightValue) const { 189 191 int rowHeight = 190 192 (rowSubtitle != nullptr) ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight; 191 193 int pageItems = rect.height / rowHeight; ··· 251 253 } 252 254 } 253 255 254 - void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const { 256 + void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const { 257 + // Hide last battery draw 258 + constexpr int maxBatteryWidth = 80; 259 + renderer.fillRect(rect.x + rect.width - maxBatteryWidth, rect.y + 5, maxBatteryWidth, 260 + BaseMetrics::values.batteryHeight + 10, false); 261 + 255 262 const bool showBatteryPercentage = 256 263 SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS; 257 264 // Position icon at right edge, drawBatteryRight will place text to the left ··· 267 274 EpdFontFamily::BOLD); 268 275 renderer.drawCenteredText(UI_12_FONT_ID, rect.y + 5, truncatedTitle.c_str(), true, EpdFontFamily::BOLD); 269 276 } 277 + 278 + if (subtitle) { 279 + auto truncatedSubtitle = renderer.truncatedText( 280 + SMALL_FONT_ID, subtitle, rect.width - BaseMetrics::values.contentSidePadding * 2, EpdFontFamily::REGULAR); 281 + int truncatedSubtitleWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedSubtitle.c_str()); 282 + renderer.drawText(SMALL_FONT_ID, 283 + rect.x + rect.width - BaseMetrics::values.contentSidePadding - truncatedSubtitleWidth, subtitleY, 284 + truncatedSubtitle.c_str(), true); 285 + } 286 + } 287 + 288 + void BaseTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, const char* rightLabel) const { 289 + constexpr int underlineHeight = 2; // Height of selection underline 290 + constexpr int underlineGap = 4; // Gap between text and underline 291 + constexpr int maxListValueWidth = 200; 292 + 293 + int currentX = rect.x + BaseMetrics::values.contentSidePadding; 294 + int rightSpace = BaseMetrics::values.contentSidePadding; 295 + if (rightLabel) { 296 + auto truncatedRightLabel = 297 + renderer.truncatedText(SMALL_FONT_ID, rightLabel, maxListValueWidth, EpdFontFamily::REGULAR); 298 + int rightLabelWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedRightLabel.c_str()); 299 + renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - BaseMetrics::values.contentSidePadding - rightLabelWidth, 300 + rect.y + 7, truncatedRightLabel.c_str()); 301 + rightSpace += rightLabelWidth + 10; 302 + } 303 + 304 + auto truncatedLabel = renderer.truncatedText( 305 + UI_12_FONT_ID, label, rect.width - BaseMetrics::values.contentSidePadding - rightSpace, EpdFontFamily::REGULAR); 306 + renderer.drawText(UI_12_FONT_ID, currentX, rect.y, truncatedLabel.c_str(), true, EpdFontFamily::REGULAR); 270 307 } 271 308 272 309 void BaseTheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const std::vector<TabInfo>& tabs, ··· 668 705 const int barWidth = progressBarMaxWidth * bookProgress / 100; 669 706 renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BaseMetrics::values.bookProgressBarHeight, true); 670 707 } 708 + 709 + void BaseTheme::drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const { 710 + auto metrics = UITheme::getInstance().getMetrics(); 711 + auto truncatedLabel = 712 + renderer.truncatedText(SMALL_FONT_ID, label, rect.width - metrics.contentSidePadding * 2, EpdFontFamily::REGULAR); 713 + renderer.drawCenteredText(SMALL_FONT_ID, rect.y, truncatedLabel.c_str()); 714 + } 715 + 716 + void BaseTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const { 717 + renderer.drawText(UI_12_FONT_ID, rect.x + 10, rect.y, "["); 718 + renderer.drawText(UI_12_FONT_ID, rect.x + rect.width - 15, rect.y + rect.height, "]"); 719 + } 720 + 721 + void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, 722 + const bool isSelected) const { 723 + const int itemWidth = renderer.getTextWidth(UI_10_FONT_ID, label); 724 + const int textX = rect.x + (rect.width - itemWidth) / 2; 725 + if (isSelected) { 726 + renderer.drawText(UI_10_FONT_ID, textX - 6, rect.y, "["); 727 + renderer.drawText(UI_10_FONT_ID, textX + itemWidth, rect.y, "]"); 728 + } 729 + renderer.drawText(UI_10_FONT_ID, textX, rect.y, label); 730 + }
+25 -11
src/components/themes/BaseTheme.h
··· 51 51 int buttonHintsHeight; 52 52 int sideButtonHintsWidth; 53 53 54 - int versionTextRightX; 55 - int versionTextY; 54 + int progressBarHeight; 55 + int bookProgressBarHeight; 56 56 57 - int bookProgressBarHeight; 57 + int keyboardKeyWidth; 58 + int keyboardKeyHeight; 59 + int keyboardKeySpacing; 60 + bool keyboardBottomAligned; 61 + bool keyboardCenteredText; 58 62 }; 59 63 60 64 // Default theme implementation (Classic Theme) ··· 82 86 .homeRecentBooksCount = 1, 83 87 .buttonHintsHeight = 40, 84 88 .sideButtonHintsWidth = 30, 85 - .versionTextRightX = 20, 86 - .versionTextY = 738, 87 - .bookProgressBarHeight = 4}; 89 + .progressBarHeight = 16, 90 + .bookProgressBarHeight = 4, 91 + .keyboardKeyWidth = 18, 92 + .keyboardKeyHeight = 18, 93 + .keyboardKeySpacing = 3, 94 + .keyboardBottomAligned = false, 95 + .keyboardCenteredText = false}; 88 96 } 89 97 90 98 class BaseTheme { ··· 102 110 virtual void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const; 103 111 virtual void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, 104 112 const std::function<std::string(int index)>& rowTitle, 105 - const std::function<std::string(int index)>& rowSubtitle, 106 - const std::function<std::string(int index)>& rowIcon, 107 - const std::function<std::string(int index)>& rowValue) const; 108 - 109 - virtual void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const; 113 + const std::function<std::string(int index)>& rowSubtitle = nullptr, 114 + const std::function<std::string(int index)>& rowIcon = nullptr, 115 + const std::function<std::string(int index)>& rowValue = nullptr, 116 + bool highlightValue = false) const; 117 + virtual void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, 118 + const char* subtitle = nullptr) const; 119 + virtual void drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, 120 + const char* rightLabel = nullptr) const; 110 121 virtual void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs, 111 122 bool selected) const; 112 123 virtual void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks, ··· 118 129 virtual Rect drawPopup(const GfxRenderer& renderer, const char* message) const; 119 130 virtual void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const; 120 131 virtual void drawReadingProgressBar(const GfxRenderer& renderer, const size_t bookProgress) const; 132 + virtual void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const; 133 + virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const; 134 + virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const; 121 135 };
+104
src/components/themes/lyra/Lyra3CoversTheme.cpp
··· 1 + #include "Lyra3CoversTheme.h" 2 + 3 + #include <GfxRenderer.h> 4 + #include <HalStorage.h> 5 + 6 + #include <cstdint> 7 + #include <string> 8 + 9 + #include "Battery.h" 10 + #include "RecentBooksStore.h" 11 + #include "components/UITheme.h" 12 + #include "fontIds.h" 13 + #include "util/StringUtils.h" 14 + 15 + // Internal constants 16 + namespace { 17 + constexpr int hPaddingInSelection = 8; 18 + constexpr int cornerRadius = 6; 19 + int coverWidth = 0; 20 + } // namespace 21 + 22 + void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks, 23 + const int selectorIndex, bool& coverRendered, bool& coverBufferStored, 24 + bool& bufferRestored, std::function<bool()> storeCoverBuffer) const { 25 + const int tileWidth = (rect.width - 2 * Lyra3CoversMetrics::values.contentSidePadding) / 3; 26 + const int tileHeight = rect.height; 27 + const int bookTitleHeight = tileHeight - Lyra3CoversMetrics::values.homeCoverHeight - hPaddingInSelection; 28 + const int tileY = rect.y; 29 + const bool hasContinueReading = !recentBooks.empty(); 30 + 31 + // Draw book card regardless, fill with message based on `hasContinueReading` 32 + // Draw cover image as background if available (inside the box) 33 + // Only load from SD on first render, then use stored buffer 34 + if (hasContinueReading) { 35 + if (!coverRendered) { 36 + for (int i = 0; 37 + i < std::min(static_cast<int>(recentBooks.size()), Lyra3CoversMetrics::values.homeRecentBooksCount); i++) { 38 + std::string coverPath = recentBooks[i].coverBmpPath; 39 + bool hasCover = true; 40 + int tileX = Lyra3CoversMetrics::values.contentSidePadding + tileWidth * i; 41 + if (coverPath.empty()) { 42 + hasCover = false; 43 + } else { 44 + const std::string coverBmpPath = 45 + UITheme::getCoverThumbPath(coverPath, Lyra3CoversMetrics::values.homeCoverHeight); 46 + 47 + // First time: load cover from SD and render 48 + FsFile file; 49 + if (Storage.openFileForRead("HOME", coverBmpPath, file)) { 50 + Bitmap bitmap(file); 51 + if (bitmap.parseHeaders() == BmpReaderError::Ok) { 52 + float coverHeight = static_cast<float>(bitmap.getHeight()); 53 + float coverWidth = static_cast<float>(bitmap.getWidth()); 54 + float ratio = coverWidth / coverHeight; 55 + const float tileRatio = static_cast<float>(tileWidth - 2 * hPaddingInSelection) / 56 + static_cast<float>(Lyra3CoversMetrics::values.homeCoverHeight); 57 + float cropX = 1.0f - (tileRatio / ratio); 58 + 59 + renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, 60 + tileWidth - 2 * hPaddingInSelection, Lyra3CoversMetrics::values.homeCoverHeight, 61 + cropX); 62 + } else { 63 + hasCover = false; 64 + } 65 + file.close(); 66 + } 67 + } 68 + 69 + if (!hasCover) { 70 + renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, 71 + tileWidth - 2 * hPaddingInSelection, Lyra3CoversMetrics::values.homeCoverHeight); 72 + } 73 + } 74 + 75 + coverBufferStored = storeCoverBuffer(); 76 + coverRendered = true; 77 + } 78 + 79 + for (int i = 0; i < std::min(static_cast<int>(recentBooks.size()), Lyra3CoversMetrics::values.homeRecentBooksCount); 80 + i++) { 81 + bool bookSelected = (selectorIndex == i); 82 + 83 + int tileX = Lyra3CoversMetrics::values.contentSidePadding + tileWidth * i; 84 + auto title = 85 + renderer.truncatedText(UI_10_FONT_ID, recentBooks[i].title.c_str(), tileWidth - 2 * hPaddingInSelection); 86 + 87 + if (bookSelected) { 88 + // Draw selection box 89 + renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false, 90 + Color::LightGray); 91 + renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection, 92 + Lyra3CoversMetrics::values.homeCoverHeight, Color::LightGray); 93 + renderer.fillRectDither(tileX + tileWidth - hPaddingInSelection, tileY + hPaddingInSelection, 94 + hPaddingInSelection, Lyra3CoversMetrics::values.homeCoverHeight, Color::LightGray); 95 + renderer.fillRoundedRect(tileX, tileY + Lyra3CoversMetrics::values.homeCoverHeight + hPaddingInSelection, 96 + tileWidth, bookTitleHeight, cornerRadius, false, false, true, true, Color::LightGray); 97 + } 98 + renderer.drawText(UI_10_FONT_ID, tileX + hPaddingInSelection, 99 + tileY + tileHeight - bookTitleHeight + hPaddingInSelection + 5, title.c_str(), true); 100 + } 101 + } else { 102 + drawEmptyRecents(renderer, rect); 103 + } 104 + }
+41
src/components/themes/lyra/Lyra3CoversTheme.h
··· 1 + 2 + 3 + #pragma once 4 + 5 + #include "components/themes/lyra/LyraTheme.h" 6 + 7 + class GfxRenderer; 8 + 9 + // Lyra theme metrics (zero runtime cost) 10 + namespace Lyra3CoversMetrics { 11 + constexpr ThemeMetrics values = {.batteryWidth = 16, 12 + .batteryHeight = 12, 13 + .topPadding = 5, 14 + .batteryBarHeight = 40, 15 + .headerHeight = 84, 16 + .verticalSpacing = 16, 17 + .contentSidePadding = 20, 18 + .listRowHeight = 40, 19 + .listWithSubtitleRowHeight = 60, 20 + .menuRowHeight = 64, 21 + .menuSpacing = 8, 22 + .tabSpacing = 8, 23 + .tabBarHeight = 40, 24 + .scrollBarWidth = 4, 25 + .scrollBarRightOffset = 5, 26 + .homeTopPadding = 56, 27 + .homeCoverHeight = 226, 28 + .homeCoverTileHeight = 287, 29 + .homeRecentBooksCount = 3, 30 + .buttonHintsHeight = 40, 31 + .sideButtonHintsWidth = 30, 32 + .progressBarHeight = 16, 33 + .bookProgressBarHeight = 4}; 34 + } 35 + 36 + class Lyra3CoversTheme : public LyraTheme { 37 + public: 38 + void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks, 39 + const int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored, 40 + std::function<bool()> storeCoverBuffer) const override; 41 + };
+136 -69
src/components/themes/lyra/LyraTheme.cpp
··· 2 2 3 3 #include <GfxRenderer.h> 4 4 #include <HalStorage.h> 5 + #include <I18n.h> 5 6 6 7 #include <cstdint> 7 8 #include <string> ··· 20 21 constexpr int topHintButtonY = 345; 21 22 constexpr int popupMarginX = 16; 22 23 constexpr int popupMarginY = 12; 24 + constexpr int maxSubtitleWidth = 100; 25 + constexpr int maxListValueWidth = 200; 26 + int coverWidth = 0; 23 27 } // namespace 24 28 25 29 void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { ··· 101 105 } 102 106 } 103 107 104 - void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const { 108 + void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const { 105 109 renderer.fillRect(rect.x, rect.y, rect.width, rect.height, false); 106 110 107 111 const bool showBatteryPercentage = ··· 112 116 Rect{batteryX, rect.y + 5, LyraMetrics::values.batteryWidth, LyraMetrics::values.batteryHeight}, 113 117 showBatteryPercentage); 114 118 119 + int maxTitleWidth = 120 + rect.width - LyraMetrics::values.contentSidePadding * 2 - (subtitle != nullptr ? maxSubtitleWidth : 0); 121 + 115 122 if (title) { 116 - auto truncatedTitle = renderer.truncatedText( 117 - UI_12_FONT_ID, title, rect.width - LyraMetrics::values.contentSidePadding * 2, EpdFontFamily::BOLD); 123 + auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title, maxTitleWidth, EpdFontFamily::BOLD); 118 124 renderer.drawText(UI_12_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding, 119 125 rect.y + LyraMetrics::values.batteryBarHeight + 3, truncatedTitle.c_str(), true, 120 126 EpdFontFamily::BOLD); 121 127 renderer.drawLine(rect.x, rect.y + rect.height - 3, rect.x + rect.width, rect.y + rect.height - 3, 3, true); 122 128 } 129 + 130 + if (subtitle) { 131 + auto truncatedSubtitle = renderer.truncatedText(SMALL_FONT_ID, subtitle, maxSubtitleWidth, EpdFontFamily::REGULAR); 132 + int truncatedSubtitleWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedSubtitle.c_str()); 133 + renderer.drawText(SMALL_FONT_ID, 134 + rect.x + rect.width - LyraMetrics::values.contentSidePadding - truncatedSubtitleWidth, 135 + rect.y + 50, truncatedSubtitle.c_str(), true); 136 + } 137 + } 138 + 139 + void LyraTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, const char* rightLabel) const { 140 + int currentX = rect.x + LyraMetrics::values.contentSidePadding; 141 + int rightSpace = LyraMetrics::values.contentSidePadding; 142 + if (rightLabel) { 143 + auto truncatedRightLabel = 144 + renderer.truncatedText(SMALL_FONT_ID, rightLabel, maxListValueWidth, EpdFontFamily::REGULAR); 145 + int rightLabelWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedRightLabel.c_str()); 146 + renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - LyraMetrics::values.contentSidePadding - rightLabelWidth, 147 + rect.y + 7, truncatedRightLabel.c_str()); 148 + rightSpace += rightLabelWidth + hPaddingInSelection; 149 + } 150 + 151 + auto truncatedLabel = renderer.truncatedText( 152 + UI_10_FONT_ID, label, rect.width - LyraMetrics::values.contentSidePadding - rightSpace, EpdFontFamily::REGULAR); 153 + renderer.drawText(UI_10_FONT_ID, currentX, rect.y + 6, truncatedLabel.c_str(), true, EpdFontFamily::REGULAR); 154 + 155 + renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width, rect.y + rect.height - 1, true); 123 156 } 124 157 125 158 void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs, ··· 158 191 const std::function<std::string(int index)>& rowTitle, 159 192 const std::function<std::string(int index)>& rowSubtitle, 160 193 const std::function<std::string(int index)>& rowIcon, 161 - const std::function<std::string(int index)>& rowValue) const { 194 + const std::function<std::string(int index)>& rowValue, bool highlightValue) const { 162 195 int rowHeight = 163 196 (rowSubtitle != nullptr) ? LyraMetrics::values.listWithSubtitleRowHeight : LyraMetrics::values.listRowHeight; 164 197 int pageItems = rect.height / rowHeight; ··· 193 226 const int itemY = rect.y + (i % pageItems) * rowHeight; 194 227 195 228 // Draw name 196 - int textWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2 - 197 - (rowValue != nullptr ? 60 : 0); // TODO truncate according to value width? 229 + int valueWidth = 0; 230 + std::string valueText = ""; 231 + if (rowValue != nullptr) { 232 + valueText = rowValue(i); 233 + valueText = renderer.truncatedText(UI_10_FONT_ID, valueText.c_str(), maxListValueWidth); 234 + valueWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()) + hPaddingInSelection; 235 + } 236 + int textWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2 - valueWidth; 198 237 auto itemName = rowTitle(i); 199 238 auto item = renderer.truncatedText(UI_10_FONT_ID, itemName.c_str(), textWidth); 200 239 renderer.drawText(UI_10_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection * 2, ··· 208 247 itemY + 30, subtitle.c_str(), true); 209 248 } 210 249 211 - if (rowValue != nullptr) { 212 - // Draw value 213 - std::string valueText = rowValue(i); 214 - if (!valueText.empty()) { 215 - const auto valueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()); 250 + // Draw value 251 + if (!valueText.empty()) { 252 + if (i == selectedIndex && highlightValue) { 253 + renderer.fillRoundedRect( 254 + contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - valueWidth, itemY, 255 + valueWidth + hPaddingInSelection, rowHeight, cornerRadius, Color::Black); 256 + } 216 257 217 - if (i == selectedIndex) { 218 - renderer.fillRoundedRect( 219 - contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection * 2 - valueTextWidth, itemY, 220 - valueTextWidth + hPaddingInSelection * 2, rowHeight, cornerRadius, Color::Black); 221 - } 222 - 223 - renderer.drawText(UI_10_FONT_ID, 224 - contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - valueTextWidth, 225 - itemY + 6, valueText.c_str(), i != selectedIndex); 226 - } 258 + renderer.drawText(UI_10_FONT_ID, rect.x + contentWidth - LyraMetrics::values.contentSidePadding - valueWidth, 259 + itemY + 6, valueText.c_str(), !(i == selectedIndex && highlightValue)); 227 260 } 228 261 } 229 262 } ··· 302 335 void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks, 303 336 const int selectorIndex, bool& coverRendered, bool& coverBufferStored, 304 337 bool& bufferRestored, std::function<bool()> storeCoverBuffer) const { 305 - const int tileWidth = (rect.width - 2 * LyraMetrics::values.contentSidePadding) / 3; 338 + const int tileWidth = rect.width - 2 * LyraMetrics::values.contentSidePadding; 306 339 const int tileHeight = rect.height; 307 - const int bookTitleHeight = tileHeight - LyraMetrics::values.homeCoverHeight - hPaddingInSelection; 308 340 const int tileY = rect.y; 309 341 const bool hasContinueReading = !recentBooks.empty(); 342 + if (coverWidth == 0) { 343 + coverWidth = LyraMetrics::values.homeCoverHeight * 0.6; 344 + } 310 345 311 346 // Draw book card regardless, fill with message based on `hasContinueReading` 312 347 // Draw cover image as background if available (inside the box) 313 348 // Only load from SD on first render, then use stored buffer 314 349 if (hasContinueReading) { 350 + RecentBook book = recentBooks[0]; 315 351 if (!coverRendered) { 316 - for (int i = 0; i < std::min(static_cast<int>(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount); 317 - i++) { 318 - std::string coverPath = recentBooks[i].coverBmpPath; 319 - int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i; 320 - renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, tileWidth - 2 * hPaddingInSelection, 321 - LyraMetrics::values.homeCoverHeight); 322 - if (!coverPath.empty()) { 323 - const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::values.homeCoverHeight); 352 + std::string coverPath = book.coverBmpPath; 353 + bool hasCover = true; 354 + int tileX = LyraMetrics::values.contentSidePadding; 355 + if (coverPath.empty()) { 356 + hasCover = false; 357 + } else { 358 + const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::values.homeCoverHeight); 324 359 325 - // First time: load cover from SD and render 326 - FsFile file; 327 - if (Storage.openFileForRead("HOME", coverBmpPath, file)) { 328 - Bitmap bitmap(file); 329 - if (bitmap.parseHeaders() == BmpReaderError::Ok) { 330 - float coverHeight = static_cast<float>(bitmap.getHeight()); 331 - float coverWidth = static_cast<float>(bitmap.getWidth()); 332 - float ratio = coverWidth / coverHeight; 333 - const float tileRatio = static_cast<float>(tileWidth - 2 * hPaddingInSelection) / 334 - static_cast<float>(LyraMetrics::values.homeCoverHeight); 335 - float cropX = 1.0f - (tileRatio / ratio); 336 - renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, 337 - tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight, cropX); 338 - } 339 - file.close(); 360 + // First time: load cover from SD and render 361 + FsFile file; 362 + if (Storage.openFileForRead("HOME", coverBmpPath, file)) { 363 + Bitmap bitmap(file); 364 + if (bitmap.parseHeaders() == BmpReaderError::Ok) { 365 + coverWidth = bitmap.getWidth(); 366 + renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth, 367 + LyraMetrics::values.homeCoverHeight); 368 + } else { 369 + hasCover = false; 340 370 } 371 + file.close(); 341 372 } 342 373 } 343 374 375 + if (!hasCover) { 376 + renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth, 377 + LyraMetrics::values.homeCoverHeight); 378 + } 379 + 344 380 coverBufferStored = storeCoverBuffer(); 345 381 coverRendered = true; 346 382 } 347 383 348 - for (int i = 0; i < std::min(static_cast<int>(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount); i++) { 349 - bool bookSelected = (selectorIndex == i); 384 + bool bookSelected = (selectorIndex == 0); 350 385 351 - int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i; 352 - auto title = 353 - renderer.truncatedText(UI_10_FONT_ID, recentBooks[i].title.c_str(), tileWidth - 2 * hPaddingInSelection); 386 + int tileX = LyraMetrics::values.contentSidePadding; 387 + int textWidth = tileWidth - 2 * hPaddingInSelection - LyraMetrics::values.verticalSpacing - coverWidth; 354 388 355 - if (bookSelected) { 356 - // Draw selection box 357 - renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false, 358 - Color::LightGray); 359 - renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection, 360 - LyraMetrics::values.homeCoverHeight, Color::LightGray); 361 - renderer.fillRectDither(tileX + tileWidth - hPaddingInSelection, tileY + hPaddingInSelection, 362 - hPaddingInSelection, LyraMetrics::values.homeCoverHeight, Color::LightGray); 363 - renderer.fillRoundedRect(tileX, tileY + LyraMetrics::values.homeCoverHeight + hPaddingInSelection, tileWidth, 364 - bookTitleHeight, cornerRadius, false, false, true, true, Color::LightGray); 365 - } 366 - renderer.drawText(UI_10_FONT_ID, tileX + hPaddingInSelection, 367 - tileY + tileHeight - bookTitleHeight + hPaddingInSelection + 5, title.c_str(), true); 389 + if (bookSelected) { 390 + // Draw selection box 391 + renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false, 392 + Color::LightGray); 393 + renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection, 394 + LyraMetrics::values.homeCoverHeight, Color::LightGray); 395 + renderer.fillRectDither(tileX + hPaddingInSelection + coverWidth, tileY + hPaddingInSelection, 396 + tileWidth - hPaddingInSelection - coverWidth, LyraMetrics::values.homeCoverHeight, 397 + Color::LightGray); 398 + renderer.fillRoundedRect(tileX, tileY + LyraMetrics::values.homeCoverHeight + hPaddingInSelection, tileWidth, 399 + hPaddingInSelection, cornerRadius, false, false, true, true, Color::LightGray); 368 400 } 401 + 402 + auto title = renderer.truncatedText(UI_12_FONT_ID, book.title.c_str(), textWidth, EpdFontFamily::BOLD); 403 + auto author = renderer.truncatedText(UI_10_FONT_ID, book.author.c_str(), textWidth); 404 + auto bookTitleHeight = renderer.getTextHeight(UI_12_FONT_ID); 405 + renderer.drawText(UI_12_FONT_ID, tileX + hPaddingInSelection + coverWidth + LyraMetrics::values.verticalSpacing, 406 + tileY + tileHeight / 2 - bookTitleHeight, title.c_str(), true, EpdFontFamily::BOLD); 407 + renderer.drawText(UI_10_FONT_ID, tileX + hPaddingInSelection + coverWidth + LyraMetrics::values.verticalSpacing, 408 + tileY + tileHeight / 2 + 5, author.c_str(), true); 409 + } else { 410 + drawEmptyRecents(renderer, rect); 369 411 } 370 412 } 371 413 414 + void LyraTheme::drawEmptyRecents(const GfxRenderer& renderer, const Rect rect) const { 415 + constexpr int padding = 48; 416 + renderer.drawText(UI_12_FONT_ID, rect.x + padding, 417 + rect.y + rect.height / 2 - renderer.getLineHeight(UI_12_FONT_ID) - 2, tr(STR_NO_OPEN_BOOK), true, 418 + EpdFontFamily::BOLD); 419 + renderer.drawText(UI_10_FONT_ID, rect.x + padding, rect.y + rect.height / 2 + 2, tr(STR_START_READING), true); 420 + } 421 + 372 422 void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex, 373 423 const std::function<std::string(int index)>& buttonLabel, 374 424 const std::function<std::string(int index)>& rowIcon) const { 375 425 for (int i = 0; i < buttonCount; ++i) { 376 - int tileWidth = (rect.width - LyraMetrics::values.contentSidePadding * 2 - LyraMetrics::values.menuSpacing) / 2; 377 - Rect tileRect = 378 - Rect{rect.x + LyraMetrics::values.contentSidePadding + (LyraMetrics::values.menuSpacing + tileWidth) * (i % 2), 379 - rect.y + static_cast<int>(i / 2) * (LyraMetrics::values.menuRowHeight + LyraMetrics::values.menuSpacing), 380 - tileWidth, LyraMetrics::values.menuRowHeight}; 426 + int tileWidth = rect.width - LyraMetrics::values.contentSidePadding * 2; 427 + Rect tileRect = Rect{rect.x + LyraMetrics::values.contentSidePadding, 428 + rect.y + i * (LyraMetrics::values.menuRowHeight + LyraMetrics::values.menuSpacing), tileWidth, 429 + LyraMetrics::values.menuRowHeight}; 381 430 382 431 const bool selected = selectedIndex == i; 383 432 ··· 432 481 433 482 renderer.displayBuffer(HalDisplay::FAST_REFRESH); 434 483 } 484 + 485 + void LyraTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const { 486 + int lineY = rect.y + rect.height + renderer.getLineHeight(UI_12_FONT_ID) + LyraMetrics::values.verticalSpacing; 487 + int lineW = textWidth + hPaddingInSelection * 2; 488 + renderer.drawLine(rect.x + (rect.width - lineW) / 2, lineY, rect.x + (rect.width + lineW) / 2, lineY, 3); 489 + } 490 + 491 + void LyraTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, 492 + const bool isSelected) const { 493 + if (isSelected) { 494 + renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cornerRadius, Color::Black); 495 + } 496 + 497 + const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, label); 498 + const int textX = rect.x + (rect.width - textWidth) / 2; 499 + const int textY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2; 500 + renderer.drawText(UI_12_FONT_ID, textX, textY, label, !isSelected); 501 + }
+16 -7
src/components/themes/lyra/LyraTheme.h
··· 23 23 .scrollBarRightOffset = 5, 24 24 .homeTopPadding = 56, 25 25 .homeCoverHeight = 226, 26 - .homeCoverTileHeight = 287, 27 - .homeRecentBooksCount = 3, 26 + .homeCoverTileHeight = 242, 27 + .homeRecentBooksCount = 1, 28 28 .buttonHintsHeight = 40, 29 29 .sideButtonHintsWidth = 30, 30 - .versionTextRightX = 20, 31 - .versionTextY = 55, 32 - .bookProgressBarHeight = 4}; 30 + .progressBarHeight = 16, 31 + .bookProgressBarHeight = 4, 32 + .keyboardKeyWidth = 31, 33 + .keyboardKeyHeight = 50, 34 + .keyboardKeySpacing = 0, 35 + .keyboardBottomAligned = true, 36 + .keyboardCenteredText = true}; 33 37 } 34 38 35 39 class LyraTheme : public BaseTheme { ··· 38 42 // void drawProgressBar(const GfxRenderer& renderer, Rect rect, size_t current, size_t total) override; 39 43 void drawBatteryLeft(const GfxRenderer& renderer, Rect rect, bool showPercentage = true) const override; 40 44 void drawBatteryRight(const GfxRenderer& renderer, Rect rect, bool showPercentage = true) const override; 41 - void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const override; 45 + void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const override; 46 + void drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, 47 + const char* rightLabel = nullptr) const override; 42 48 void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs, 43 49 bool selected) const override; 44 50 void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, 45 51 const std::function<std::string(int index)>& rowTitle, 46 52 const std::function<std::string(int index)>& rowSubtitle, 47 53 const std::function<std::string(int index)>& rowIcon, 48 - const std::function<std::string(int index)>& rowValue) const override; 54 + const std::function<std::string(int index)>& rowValue, bool highlightValue) const override; 49 55 void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, 50 56 const char* btn4) const override; 51 57 void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const override; ··· 55 61 void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks, 56 62 const int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored, 57 63 std::function<bool()> storeCoverBuffer) const override; 64 + void drawEmptyRecents(const GfxRenderer& renderer, const Rect rect) const; 58 65 Rect drawPopup(const GfxRenderer& renderer, const char* message) const override; 59 66 void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const override; 67 + void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const override; 68 + void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const override; 60 69 };