feat: Lyra screens (#732)

Implements Lyra theme for some more Crosspoint screens:

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

- A bit of refactoring for list scrolling logic

---

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

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

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
This commit is contained in:
CaptainFrito
2026-02-19 17:16:55 +07:00
committed by cottongin
parent 21b81bd177
commit 724c1969b9
35 changed files with 645 additions and 531 deletions

View File

@@ -325,7 +325,9 @@ void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, con
return; return;
} }
const int maxRadius = std::min({cornerRadius, width / 2, height / 2}); // Assume if we're not rounding all corners then we are only rounding one side
const int roundedSides = (!roundTopLeft || !roundTopRight || !roundBottomLeft || !roundBottomRight) ? 1 : 2;
const int maxRadius = std::min({cornerRadius, width / roundedSides, height / roundedSides});
if (maxRadius <= 0) { if (maxRadius <= 0) {
fillRectDither(x, y, width, height, color); fillRectDither(x, y, width, height, color);
return; return;
@@ -336,10 +338,16 @@ void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, con
fillRectDither(x + maxRadius + 1, y, horizontalWidth - 2, height, color); fillRectDither(x + maxRadius + 1, y, horizontalWidth - 2, height, color);
} }
const int verticalHeight = height - 2 * maxRadius - 2; const int leftFillTop = y + (roundTopLeft ? (maxRadius + 1) : 0);
if (verticalHeight > 0) { const int leftFillBottom = y + height - 1 - (roundBottomLeft ? (maxRadius + 1) : 0);
fillRectDither(x, y + maxRadius + 1, maxRadius + 1, verticalHeight, color); if (leftFillBottom >= leftFillTop) {
fillRectDither(x + width - maxRadius - 1, y + maxRadius + 1, maxRadius + 1, verticalHeight, color); fillRectDither(x, leftFillTop, maxRadius + 1, leftFillBottom - leftFillTop + 1, color);
}
const int rightFillTop = y + (roundTopRight ? (maxRadius + 1) : 0);
const int rightFillBottom = y + height - 1 - (roundBottomRight ? (maxRadius + 1) : 0);
if (rightFillBottom >= rightFillTop) {
fillRectDither(x + width - maxRadius - 1, rightFillTop, maxRadius + 1, rightFillBottom - rightFillTop + 1, color);
} }
auto fillArcTemplated = [this](int maxRadius, int cx, int cy, int xDir, int yDir, Color color) { auto fillArcTemplated = [this](int maxRadius, int cx, int cy, int xDir, int yDir, Color color) {
@@ -363,26 +371,18 @@ void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, con
if (roundTopLeft) { if (roundTopLeft) {
fillArcTemplated(maxRadius, x + maxRadius, y + maxRadius, -1, -1, color); fillArcTemplated(maxRadius, x + maxRadius, y + maxRadius, -1, -1, color);
} else {
fillRectDither(x, y, maxRadius + 1, maxRadius + 1, color);
} }
if (roundTopRight) { if (roundTopRight) {
fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + maxRadius, 1, -1, color); fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + maxRadius, 1, -1, color);
} else {
fillRectDither(x + width - maxRadius - 1, y, maxRadius + 1, maxRadius + 1, color);
} }
if (roundBottomRight) { if (roundBottomRight) {
fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + height - maxRadius - 1, 1, 1, color); fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + height - maxRadius - 1, 1, 1, color);
} else {
fillRectDither(x + width - maxRadius - 1, y + height - maxRadius - 1, maxRadius + 1, maxRadius + 1, color);
} }
if (roundBottomLeft) { if (roundBottomLeft) {
fillArcTemplated(maxRadius, x + maxRadius, y + height - maxRadius - 1, -1, 1, color); fillArcTemplated(maxRadius, x + maxRadius, y + height - maxRadius - 1, -1, 1, color);
} else {
fillRectDither(x, y + height - maxRadius - 1, maxRadius + 1, maxRadius + 1, color);
} }
} }

View File

@@ -290,6 +290,7 @@ enum class StrId : uint16_t {
STR_UI_THEME, STR_UI_THEME,
STR_THEME_CLASSIC, STR_THEME_CLASSIC,
STR_THEME_LYRA, STR_THEME_LYRA,
STR_THEME_LYRA_EXTENDED,
STR_SUNLIGHT_FADING_FIX, STR_SUNLIGHT_FADING_FIX,
STR_REMAP_FRONT_BUTTONS, STR_REMAP_FRONT_BUTTONS,
STR_OPDS_BROWSER, STR_OPDS_BROWSER,

View File

@@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Plná s pruhem kapitol"
STR_UI_THEME: "Šablona rozhraní" STR_UI_THEME: "Šablona rozhraní"
STR_THEME_CLASSIC: "Klasická" STR_THEME_CLASSIC: "Klasická"
STR_THEME_LYRA: "Lyra" STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Oprava blednutí na slunci" STR_SUNLIGHT_FADING_FIX: "Oprava blednutí na slunci"
STR_REMAP_FRONT_BUTTONS: "Přemapovat přední tlačítka" STR_REMAP_FRONT_BUTTONS: "Přemapovat přední tlačítka"
STR_OPDS_BROWSER: "Prohlížeč OPDS" STR_OPDS_BROWSER: "Prohlížeč OPDS"

View File

@@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Full w/ Chapter Bar"
STR_UI_THEME: "UI Theme" STR_UI_THEME: "UI Theme"
STR_THEME_CLASSIC: "Classic" STR_THEME_CLASSIC: "Classic"
STR_THEME_LYRA: "Lyra" STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix" STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons" STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
STR_OPDS_BROWSER: "OPDS Browser" STR_OPDS_BROWSER: "OPDS Browser"

View File

@@ -47,7 +47,7 @@ STR_HOW_CONNECT: "Comment voulez-vous vous connecter ?"
STR_JOIN_NETWORK: "Connexion à un réseau" STR_JOIN_NETWORK: "Connexion à un réseau"
STR_CREATE_HOTSPOT: "Créer un point daccès" STR_CREATE_HOTSPOT: "Créer un point daccès"
STR_JOIN_DESC: "Se connecter à un réseau WiFi existant" STR_JOIN_DESC: "Se connecter à un réseau WiFi existant"
STR_HOTSPOT_DESC: "Créer un réseau WiFi accessible depuis dautres appareils" STR_HOTSPOT_DESC: "Créer un réseau WiFi accessible sur dautres appareils"
STR_STARTING_HOTSPOT: "Création du point daccès en cours…" STR_STARTING_HOTSPOT: "Création du point daccès en cours…"
STR_HOTSPOT_MODE: "Mode point daccès" STR_HOTSPOT_MODE: "Mode point daccès"
STR_CONNECT_WIFI_HINT: "Connectez un appareil à ce réseau WiFi" STR_CONNECT_WIFI_HINT: "Connectez un appareil à ce réseau WiFi"
@@ -80,7 +80,7 @@ STR_CALIBRE_TRANSFER_INTERRUPTED: "Transfert interrompu"
STR_CALIBRE_INSTRUCTION_1: "1) Installer le plugin CrossPoint Reader" STR_CALIBRE_INSTRUCTION_1: "1) Installer le plugin CrossPoint Reader"
STR_CALIBRE_INSTRUCTION_2: "2) Se connecter au même réseau WiFi" STR_CALIBRE_INSTRUCTION_2: "2) Se connecter au même réseau WiFi"
STR_CALIBRE_INSTRUCTION_3: "3) Dans Calibre : Envoyer vers lappareil" STR_CALIBRE_INSTRUCTION_3: "3) Dans Calibre : Envoyer vers lappareil"
STR_CALIBRE_INSTRUCTION_4: "Gardez cet écran ouvert pendant le transfert" STR_CALIBRE_INSTRUCTION_4: "4) Gardez cet écran ouvert pendant le transfert"
STR_CAT_DISPLAY: "Affichage" STR_CAT_DISPLAY: "Affichage"
STR_CAT_READER: "Lecteur" STR_CAT_READER: "Lecteur"
STR_CAT_CONTROLS: "Commandes" STR_CAT_CONTROLS: "Commandes"
@@ -215,7 +215,7 @@ STR_NO_SERVER_URL: "Aucune URL serveur configurée"
STR_FETCH_FEED_FAILED: "Échec du téléchargement du flux" STR_FETCH_FEED_FAILED: "Échec du téléchargement du flux"
STR_PARSE_FEED_FAILED: "Échec de lanalyse du flux" STR_PARSE_FEED_FAILED: "Échec de lanalyse du flux"
STR_NETWORK_PREFIX: "Réseau : " STR_NETWORK_PREFIX: "Réseau : "
STR_IP_ADDRESS_PREFIX: "Adresse IP : " STR_IP_ADDRESS_PREFIX: "IP : "
STR_SCAN_QR_WIFI_HINT: "or scan QR code with your phone to connect to Wifi." STR_SCAN_QR_WIFI_HINT: "or scan QR code with your phone to connect to Wifi."
STR_ERROR_GENERAL_FAILURE: "Erreur : Échec général" STR_ERROR_GENERAL_FAILURE: "Erreur : Échec général"
STR_ERROR_NETWORK_NOT_FOUND: "Erreur : Réseau introuvable" STR_ERROR_NETWORK_NOT_FOUND: "Erreur : Réseau introuvable"
@@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Complète + barre chapitre"
STR_UI_THEME: "Thème de linterface" STR_UI_THEME: "Thème de linterface"
STR_THEME_CLASSIC: "Classique" STR_THEME_CLASSIC: "Classique"
STR_THEME_LYRA: "Lyra" STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Amélioration de la lisibilité au soleil" STR_SUNLIGHT_FADING_FIX: "Amélioration de la lisibilité au soleil"
STR_REMAP_FRONT_BUTTONS: "Réassigner les boutons avant" STR_REMAP_FRONT_BUTTONS: "Réassigner les boutons avant"
STR_OPDS_BROWSER: "Navigateur OPDS" STR_OPDS_BROWSER: "Navigateur OPDS"

View File

@@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Komplett + Kapitel"
STR_UI_THEME: "System-Design" STR_UI_THEME: "System-Design"
STR_THEME_CLASSIC: "Klassisch" STR_THEME_CLASSIC: "Klassisch"
STR_THEME_LYRA: "Lyra" STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Anti-Verblassen" STR_SUNLIGHT_FADING_FIX: "Anti-Verblassen"
STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen" STR_REMAP_FRONT_BUTTONS: "Vordere Tasten belegen"
STR_OPDS_BROWSER: "OPDS-Browser" STR_OPDS_BROWSER: "OPDS-Browser"

View File

@@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Completa c/ barra capítulo"
STR_UI_THEME: "Tema da interface" STR_UI_THEME: "Tema da interface"
STR_THEME_CLASSIC: "Clássico" STR_THEME_CLASSIC: "Clássico"
STR_THEME_LYRA: "Lyra" STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol" STR_SUNLIGHT_FADING_FIX: "Ajuste desbotamento ao sol"
STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais" STR_REMAP_FRONT_BUTTONS: "Remapear botões frontais"
STR_OPDS_BROWSER: "Navegador OPDS" STR_OPDS_BROWSER: "Navegador OPDS"

View File

@@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Полная + шкала главы"
STR_UI_THEME: "Тема интерфейса" STR_UI_THEME: "Тема интерфейса"
STR_THEME_CLASSIC: "Классическая" STR_THEME_CLASSIC: "Классическая"
STR_THEME_LYRA: "Lyra" STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Компенсация выцветания" STR_SUNLIGHT_FADING_FIX: "Компенсация выцветания"
STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки" STR_REMAP_FRONT_BUTTONS: "Переназначить передние кнопки"
STR_OPDS_BROWSER: "OPDS браузер" STR_OPDS_BROWSER: "OPDS браузер"

View File

@@ -255,7 +255,8 @@ STR_STATUS_BAR_BOOK_ONLY: "Solo progreso del libro"
STR_STATUS_BAR_FULL_CHAPTER: "Completa con progreso de capítulos" STR_STATUS_BAR_FULL_CHAPTER: "Completa con progreso de capítulos"
STR_UI_THEME: "Estilo de pantalla" STR_UI_THEME: "Estilo de pantalla"
STR_THEME_CLASSIC: "Clásico" STR_THEME_CLASSIC: "Clásico"
STR_THEME_LYRA: "LYRA" STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Corrección de desvastado por sol" STR_SUNLIGHT_FADING_FIX: "Corrección de desvastado por sol"
STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales" STR_REMAP_FRONT_BUTTONS: "Reconfigurar botones frontales"
STR_OPDS_BROWSER: "Navegador opds" STR_OPDS_BROWSER: "Navegador opds"

View File

@@ -256,6 +256,7 @@ STR_STATUS_BAR_FULL_CHAPTER: "Full w/ Kapitellist"
STR_UI_THEME: "Användargränssnittstema" STR_UI_THEME: "Användargränssnittstema"
STR_THEME_CLASSIC: "Klassisk" STR_THEME_CLASSIC: "Klassisk"
STR_THEME_LYRA: "Lyra" STR_THEME_LYRA: "Lyra"
STR_THEME_LYRA_EXTENDED: "Lyra Extended"
STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning" STR_SUNLIGHT_FADING_FIX: "Fix för solskensmattning"
STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar" STR_REMAP_FRONT_BUTTONS: "Ändra frontknappar"
STR_OPDS_BROWSER: "OPDS-webbläsare" STR_OPDS_BROWSER: "OPDS-webbläsare"

View File

@@ -133,7 +133,7 @@ class CrossPointSettings {
enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT }; enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT };
// UI Theme // UI Theme
enum UI_THEME { CLASSIC = 0, LYRA = 1 }; enum UI_THEME { CLASSIC = 0, LYRA = 1, LYRA_3_COVERS = 2 };
// Home screen clock format // Home screen clock format
enum CLOCK_FORMAT { CLOCK_OFF = 0, CLOCK_AMPM = 1, CLOCK_24H = 2, CLOCK_FORMAT_COUNT }; enum CLOCK_FORMAT { CLOCK_OFF = 0, CLOCK_AMPM = 1, CLOCK_24H = 2, CLOCK_FORMAT_COUNT };

View File

@@ -77,7 +77,8 @@ inline std::vector<SettingInfo> getSettingsList() {
{StrId::STR_PAGES_1, StrId::STR_PAGES_5, StrId::STR_PAGES_10, StrId::STR_PAGES_15, StrId::STR_PAGES_30}, {StrId::STR_PAGES_1, StrId::STR_PAGES_5, StrId::STR_PAGES_10, StrId::STR_PAGES_15, StrId::STR_PAGES_30},
"refreshFrequency", StrId::STR_CAT_DISPLAY), "refreshFrequency", StrId::STR_CAT_DISPLAY),
SettingInfo::Enum(StrId::STR_UI_THEME, &CrossPointSettings::uiTheme, SettingInfo::Enum(StrId::STR_UI_THEME, &CrossPointSettings::uiTheme,
{StrId::STR_THEME_CLASSIC, StrId::STR_THEME_LYRA}, "uiTheme", StrId::STR_CAT_DISPLAY), {StrId::STR_THEME_CLASSIC, StrId::STR_THEME_LYRA, StrId::STR_THEME_LYRA_EXTENDED}, "uiTheme",
StrId::STR_CAT_DISPLAY),
SettingInfo::Toggle(StrId::STR_SUNLIGHT_FADING_FIX, &CrossPointSettings::fadingFix, "fadingFix", SettingInfo::Toggle(StrId::STR_SUNLIGHT_FADING_FIX, &CrossPointSettings::fadingFix, "fadingFix",
StrId::STR_CAT_DISPLAY), StrId::STR_CAT_DISPLAY),
// --- Clock --- // --- Clock ---

View File

@@ -167,7 +167,6 @@ void MyLibraryActivity::loop() {
} }
int listSize = static_cast<int>(files.size()); int listSize = static_cast<int>(files.size());
buttonNavigator.onNextRelease([this, listSize] { buttonNavigator.onNextRelease([this, listSize] {
selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize); selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize);
requestUpdate(); requestUpdate();

View File

@@ -169,76 +169,62 @@ void CalibreConnectActivity::loop() {
} }
void CalibreConnectActivity::render(Activity::RenderLock&&) { void CalibreConnectActivity::render(Activity::RenderLock&&) {
if (state == CalibreConnectState::SERVER_RUNNING) { auto metrics = UITheme::getInstance().getMetrics();
renderer.clearScreen(); const auto pageWidth = renderer.getScreenWidth();
renderServerRunning();
renderer.displayBuffer();
return;
}
renderer.clearScreen();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CALIBRE_WIRELESS));
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2;
if (state == CalibreConnectState::SERVER_STARTING) { if (state == CalibreConnectState::SERVER_STARTING) {
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_CALIBRE_STARTING), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, top, tr(STR_CALIBRE_STARTING));
} else if (state == CalibreConnectState::ERROR) { } else if (state == CalibreConnectState::ERROR) {
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_CONNECTION_FAILED), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, top, tr(STR_CONNECTION_FAILED), true, EpdFontFamily::BOLD);
} } else if (state == CalibreConnectState::SERVER_RUNNING) {
renderer.displayBuffer(); GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
} connectedSSID.c_str(), (std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP).c_str());
void CalibreConnectActivity::renderServerRunning() const { int y = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 4;
constexpr int LINE_SPACING = 24; const auto heightText12 = renderer.getTextHeight(UI_12_FONT_ID);
constexpr int SMALL_SPACING = 20; renderer.drawText(UI_12_FONT_ID, metrics.contentSidePadding, y, tr(STR_CALIBRE_SETUP), true, EpdFontFamily::BOLD);
constexpr int SECTION_SPACING = 40; y += heightText12 + metrics.verticalSpacing * 2;
constexpr int TOP_PADDING = 14;
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_CALIBRE_WIRELESS), true, EpdFontFamily::BOLD);
int y = 55 + TOP_PADDING; renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y, tr(STR_CALIBRE_INSTRUCTION_1));
renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_WIFI_NETWORKS), true, EpdFontFamily::BOLD); renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height, tr(STR_CALIBRE_INSTRUCTION_2));
y += LINE_SPACING; renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height * 2, tr(STR_CALIBRE_INSTRUCTION_3));
std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + connectedSSID; renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y + height * 3, tr(STR_CALIBRE_INSTRUCTION_4));
if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
}
renderer.drawCenteredText(UI_10_FONT_ID, y, ssidInfo.c_str());
renderer.drawCenteredText(UI_10_FONT_ID, y + LINE_SPACING,
(std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP).c_str());
y += LINE_SPACING * 2 + SECTION_SPACING; y += height * 3 + metrics.verticalSpacing * 4;
renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_CALIBRE_SETUP), true, EpdFontFamily::BOLD); renderer.drawText(UI_12_FONT_ID, metrics.contentSidePadding, y, tr(STR_CALIBRE_STATUS), true, EpdFontFamily::BOLD);
y += LINE_SPACING; y += heightText12 + metrics.verticalSpacing * 2;
renderer.drawCenteredText(SMALL_FONT_ID, y, tr(STR_CALIBRE_INSTRUCTION_1));
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING, tr(STR_CALIBRE_INSTRUCTION_2));
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 2, tr(STR_CALIBRE_INSTRUCTION_3));
renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 3, tr(STR_CALIBRE_INSTRUCTION_4));
y += SMALL_SPACING * 3 + SECTION_SPACING;
renderer.drawCenteredText(UI_10_FONT_ID, y, tr(STR_CALIBRE_STATUS), true, EpdFontFamily::BOLD);
y += LINE_SPACING;
if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) { if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) {
std::string label = tr(STR_CALIBRE_RECEIVING); std::string label = tr(STR_CALIBRE_RECEIVING);
if (!currentUploadName.empty()) { if (!currentUploadName.empty()) {
label += ": " + currentUploadName; label += ": " + currentUploadName;
if (label.length() > 34) { label = renderer.truncatedText(SMALL_FONT_ID, label.c_str(), pageWidth - metrics.contentSidePadding * 2,
label.replace(31, label.length() - 31, "..."); EpdFontFamily::REGULAR);
} }
} renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y, label.c_str());
renderer.drawCenteredText(SMALL_FONT_ID, y, label.c_str()); GUI.drawProgressBar(renderer,
constexpr int barWidth = 300; Rect{metrics.contentSidePadding, y + height + metrics.verticalSpacing,
constexpr int barHeight = 16; pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight},
constexpr int barX = (480 - barWidth) / 2; lastProgressReceived, lastProgressTotal);
GUI.drawProgressBar(renderer, Rect{barX, y + 22, barWidth, barHeight}, lastProgressReceived, lastProgressTotal); y += height + metrics.verticalSpacing * 2 + metrics.progressBarHeight;
y += 40;
} }
if (lastCompleteAt > 0 && (millis() - lastCompleteAt) < 6000) { if (lastCompleteAt > 0 && (millis() - lastCompleteAt) < 6000) {
std::string msg = std::string(tr(STR_CALIBRE_RECEIVED)) + lastCompleteName; std::string msg = std::string(tr(STR_CALIBRE_RECEIVED)) + lastCompleteName;
if (msg.length() > 36) { msg = renderer.truncatedText(SMALL_FONT_ID, msg.c_str(), pageWidth - metrics.contentSidePadding * 2,
msg.replace(33, msg.length() - 33, "..."); EpdFontFamily::REGULAR);
} renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding, y, msg.c_str());
renderer.drawCenteredText(SMALL_FONT_ID, y, msg.c_str());
} }
const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}
renderer.displayBuffer();
} }

View File

@@ -24,6 +24,8 @@ constexpr const char* AP_PASSWORD = nullptr; // Open network for ease of use
constexpr const char* AP_HOSTNAME = "crosspoint"; constexpr const char* AP_HOSTNAME = "crosspoint";
constexpr uint8_t AP_CHANNEL = 1; constexpr uint8_t AP_CHANNEL = 1;
constexpr uint8_t AP_MAX_CONNECTIONS = 4; constexpr uint8_t AP_MAX_CONNECTIONS = 4;
constexpr int QR_CODE_WIDTH = 6 * 33;
constexpr int QR_CODE_HEIGHT = 200;
// DNS server for captive portal (redirects all DNS queries to our IP) // DNS server for captive portal (redirects all DNS queries to our IP)
DNSServer* dnsServer = nullptr; DNSServer* dnsServer = nullptr;
@@ -339,14 +341,24 @@ void CrossPointWebServerActivity::loop() {
void CrossPointWebServerActivity::render(Activity::RenderLock&&) { void CrossPointWebServerActivity::render(Activity::RenderLock&&) {
// Only render our own UI when server is running // Only render our own UI when server is running
// Subactivities handle their own rendering // Subactivities handle their own rendering
if (state == WebServerActivityState::SERVER_RUNNING) { if (state == WebServerActivityState::SERVER_RUNNING || state == WebServerActivityState::AP_STARTING) {
renderer.clearScreen();
renderServerRunning();
renderer.displayBuffer();
} else if (state == WebServerActivityState::AP_STARTING) {
renderer.clearScreen(); renderer.clearScreen();
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, tr(STR_STARTING_HOTSPOT), true, EpdFontFamily::BOLD);
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight},
isApMode ? tr(STR_HOTSPOT_MODE) : tr(STR_FILE_TRANSFER), nullptr);
if (state == WebServerActivityState::SERVER_RUNNING) {
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
connectedSSID.c_str());
renderServerRunning();
} else {
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2;
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_STARTING_HOTSPOT));
}
renderer.displayBuffer(); renderer.displayBuffer();
} }
} }
@@ -374,66 +386,70 @@ void drawQRCode(const GfxRenderer& renderer, const int x, const int y, const std
} }
void CrossPointWebServerActivity::renderServerRunning() const { void CrossPointWebServerActivity::renderServerRunning() const {
// Use consistent line spacing auto metrics = UITheme::getInstance().getMetrics();
constexpr int LINE_SPACING = 28; // Space between lines const auto pageWidth = renderer.getScreenWidth();
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_FILE_TRANSFER), true, EpdFontFamily::BOLD); GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight},
isApMode ? tr(STR_HOTSPOT_MODE) : tr(STR_FILE_TRANSFER), nullptr);
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
connectedSSID.c_str());
int startY = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing * 2;
int height10 = renderer.getLineHeight(UI_10_FONT_ID);
if (isApMode) { if (isApMode) {
// AP mode display - center the content block // AP mode display
int startY = 55; renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, startY, tr(STR_CONNECT_WIFI_HINT), true,
EpdFontFamily::BOLD);
startY += height10 + metrics.verticalSpacing * 2;
renderer.drawCenteredText(UI_10_FONT_ID, startY, tr(STR_HOTSPOT_MODE), true, EpdFontFamily::BOLD); // Show QR code for Wifi
std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + connectedSSID;
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ssidInfo.c_str());
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 2, tr(STR_CONNECT_WIFI_HINT));
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, tr(STR_SCAN_QR_WIFI_HINT));
// Show QR code for URL
const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;"; const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;";
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 4, wifiConfig); drawQRCode(renderer, metrics.contentSidePadding, startY, wifiConfig);
// Show network name
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 80,
connectedSSID.c_str());
startY += QR_CODE_HEIGHT + 2 * metrics.verticalSpacing;
startY += 6 * 29 + 3 * LINE_SPACING;
// Show primary URL (hostname) // Show primary URL (hostname)
renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, startY, tr(STR_OPEN_URL_HINT), true,
EpdFontFamily::BOLD);
startY += height10 + metrics.verticalSpacing * 2;
std::string hostnameUrl = std::string("http://") + AP_HOSTNAME + ".local/"; std::string hostnameUrl = std::string("http://") + AP_HOSTNAME + ".local/";
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str(), true, EpdFontFamily::BOLD); std::string ipUrl = tr(STR_OR_HTTP_PREFIX) + connectedIP + "/";
// Show QR code for URL
drawQRCode(renderer, metrics.contentSidePadding, startY, hostnameUrl);
// Show IP address as fallback // Show IP address as fallback
std::string ipUrl = std::string(tr(STR_OR_HTTP_PREFIX)) + connectedIP + "/"; renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 80,
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, ipUrl.c_str()); hostnameUrl.c_str());
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, tr(STR_OPEN_URL_HINT)); renderer.drawText(SMALL_FONT_ID, metrics.contentSidePadding + QR_CODE_WIDTH + metrics.verticalSpacing, startY + 100,
ipUrl.c_str());
} else {
startY += metrics.verticalSpacing * 2;
// STA mode display (original behavior)
// std::string ipInfo = "IP Address: " + connectedIP;
renderer.drawCenteredText(UI_10_FONT_ID, startY, tr(STR_OPEN_URL_HINT), true, EpdFontFamily::BOLD);
startY += height10;
renderer.drawCenteredText(UI_10_FONT_ID, startY, tr(STR_SCAN_QR_HINT), true, EpdFontFamily::BOLD);
startY += height10 + metrics.verticalSpacing * 2;
// Show QR code for URL // Show QR code for URL
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, tr(STR_SCAN_QR_HINT)); std::string webInfo = "http://" + connectedIP + "/";
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 7, hostnameUrl); drawQRCode(renderer, (pageWidth - QR_CODE_WIDTH) / 2, startY, webInfo);
} else { startY += QR_CODE_HEIGHT + metrics.verticalSpacing * 2;
// STA mode display (original behavior)
const int startY = 65;
std::string ssidInfo = std::string(tr(STR_NETWORK_PREFIX)) + connectedSSID;
if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "...");
}
renderer.drawCenteredText(UI_10_FONT_ID, startY, ssidInfo.c_str());
std::string ipInfo = std::string(tr(STR_IP_ADDRESS_PREFIX)) + connectedIP;
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ipInfo.c_str());
// Show web server URL prominently // Show web server URL prominently
std::string webInfo = "http://" + connectedIP + "/"; renderer.drawCenteredText(UI_10_FONT_ID, startY, webInfo.c_str(), true);
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 2, webInfo.c_str(), true, EpdFontFamily::BOLD); startY += height10 + 5;
// Also show hostname URL // Also show hostname URL
std::string hostnameUrl = std::string(tr(STR_OR_HTTP_PREFIX)) + AP_HOSTNAME + ".local/"; std::string hostnameUrl = std::string(tr(STR_OR_HTTP_PREFIX)) + AP_HOSTNAME + ".local/";
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str()); renderer.drawCenteredText(SMALL_FONT_ID, startY, hostnameUrl.c_str(), true);
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, tr(STR_OPEN_URL_HINT));
// Show QR code for URL
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 6, webInfo);
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, tr(STR_SCAN_QR_HINT));
} }
const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_EXIT), "", "", "");

View File

@@ -57,39 +57,24 @@ void NetworkModeSelectionActivity::loop() {
void NetworkModeSelectionActivity::render(Activity::RenderLock&&) { void NetworkModeSelectionActivity::render(Activity::RenderLock&&) {
renderer.clearScreen(); renderer.clearScreen();
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
// Draw header GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_FILE_TRANSFER));
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_FILE_TRANSFER), true, EpdFontFamily::BOLD);
// Draw subtitle
renderer.drawCenteredText(UI_10_FONT_ID, 50, tr(STR_HOW_CONNECT));
const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2;
// Menu items and descriptions // Menu items and descriptions
static constexpr StrId menuItems[MENU_ITEM_COUNT] = {StrId::STR_JOIN_NETWORK, StrId::STR_CALIBRE_WIRELESS, static constexpr StrId menuItems[MENU_ITEM_COUNT] = {StrId::STR_JOIN_NETWORK, StrId::STR_CALIBRE_WIRELESS,
StrId::STR_CREATE_HOTSPOT}; StrId::STR_CREATE_HOTSPOT};
static constexpr StrId menuDescs[MENU_ITEM_COUNT] = {StrId::STR_JOIN_DESC, StrId::STR_CALIBRE_DESC, static constexpr StrId menuDescs[MENU_ITEM_COUNT] = {StrId::STR_JOIN_DESC, StrId::STR_CALIBRE_DESC,
StrId::STR_HOTSPOT_DESC}; StrId::STR_HOTSPOT_DESC};
// Draw menu items centered on screen GUI.drawList(
constexpr int itemHeight = 50; // Height for each menu item (including description) renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(MENU_ITEM_COUNT), selectedIndex,
const int startY = (pageHeight - (MENU_ITEM_COUNT * itemHeight)) / 2 + 10; [](int index) { return std::string(I18N.get(menuItems[index])); },
[](int index) { return std::string(I18N.get(menuDescs[index])); });
for (int i = 0; i < MENU_ITEM_COUNT; i++) {
const int itemY = startY + i * itemHeight;
const bool isSelected = (i == selectedIndex);
// Draw selection highlight (black fill) for selected item
if (isSelected) {
renderer.fillRect(20, itemY - 2, pageWidth - 40, itemHeight - 6);
}
// Draw text: black=false (white text) when selected (on black background)
// black=true (black text) when not selected (on white background)
renderer.drawText(UI_10_FONT_ID, 30, itemY, I18N.get(menuItems[i]), /*black=*/!isSelected);
renderer.drawText(SMALL_FONT_ID, 30, itemY + 22, I18N.get(menuDescs[i]), /*black=*/!isSelected);
}
// Draw help text at bottom // Draw help text at bottom
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", "");

View File

@@ -150,13 +150,12 @@ void WifiSelectionActivity::processWifiScanResults() {
networks.push_back(pair.second); networks.push_back(pair.second);
} }
// Sort by signal strength (strongest first) // Sort: saved-password networks first, then by signal strength (strongest first)
std::sort(networks.begin(), networks.end(),
[](const WifiNetworkInfo& a, const WifiNetworkInfo& b) { return a.rssi > b.rssi; });
// Show networks with PW first
std::sort(networks.begin(), networks.end(), [](const WifiNetworkInfo& a, const WifiNetworkInfo& b) { std::sort(networks.begin(), networks.end(), [](const WifiNetworkInfo& a, const WifiNetworkInfo& b) {
return a.hasSavedPassword && !b.hasSavedPassword; if (a.hasSavedPassword != b.hasSavedPassword) {
return a.hasSavedPassword;
}
return a.rssi > b.rssi;
}); });
WiFi.scanDelete(); WiFi.scanDelete();
@@ -195,7 +194,6 @@ void WifiSelectionActivity::selectNetwork(const int index) {
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD), renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD),
"", // No initial text "", // No initial text
50, // Y position
64, // Max password length 64, // Max password length
false, // Show password by default (hard keyboard to use) false, // Show password by default (hard keyboard to use)
[this](const std::string& text) { [this](const std::string& text) {
@@ -459,15 +457,12 @@ std::string WifiSelectionActivity::getSignalStrengthIndicator(const int32_t rssi
return "||||"; // Excellent return "||||"; // Excellent
} }
if (rssi >= -60) { if (rssi >= -60) {
return "||| "; // Good return " |||"; // Good
} }
if (rssi >= -70) { if (rssi >= -70) {
return "|| "; // Fair return " ||"; // Fair
} }
if (rssi >= -80) { return " |"; // Very weak
return "| "; // Weak
}
return " "; // Very weak
} }
void WifiSelectionActivity::render(Activity::RenderLock&&) { void WifiSelectionActivity::render(Activity::RenderLock&&) {
@@ -480,6 +475,18 @@ void WifiSelectionActivity::render(Activity::RenderLock&&) {
renderer.clearScreen(); renderer.clearScreen();
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Draw header
char countStr[32];
snprintf(countStr, sizeof(countStr), tr(STR_NETWORKS_FOUND), networks.size());
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_WIFI_NETWORKS),
countStr);
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
cachedMacAddress.c_str());
switch (state) { switch (state) {
case WifiSelectionState::AUTO_CONNECTING: case WifiSelectionState::AUTO_CONNECTING:
renderConnecting(); renderConnecting();
@@ -511,12 +518,10 @@ void WifiSelectionActivity::render(Activity::RenderLock&&) {
} }
void WifiSelectionActivity::renderNetworkList() const { void WifiSelectionActivity::renderNetworkList() const {
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
// Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_WIFI_NETWORKS), true, EpdFontFamily::BOLD);
if (networks.empty()) { if (networks.empty()) {
// No networks found or scan failed // No networks found or scan failed
const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto height = renderer.getLineHeight(UI_10_FONT_ID);
@@ -524,69 +529,21 @@ void WifiSelectionActivity::renderNetworkList() const {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NO_NETWORKS)); renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NO_NETWORKS));
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, tr(STR_PRESS_OK_SCAN)); renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, tr(STR_PRESS_OK_SCAN));
} else { } else {
// Calculate how many networks we can display int contentTop = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing;
constexpr int startY = 60; int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2;
constexpr int lineHeight = 25; GUI.drawList(
const int maxVisibleNetworks = (pageHeight - startY - 40) / lineHeight; renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(networks.size()),
selectedNetworkIndex, [this](int index) { return networks[index].ssid; }, nullptr, nullptr,
// Calculate scroll offset to keep selected item visible [this](int index) {
int scrollOffset = 0; auto network = networks[index];
if (selectedNetworkIndex >= maxVisibleNetworks) { return std::string(network.hasSavedPassword ? "+ " : "") + (network.isEncrypted ? "* " : "") +
scrollOffset = selectedNetworkIndex - maxVisibleNetworks + 1; getSignalStrengthIndicator(network.rssi);
});
} }
// Draw networks GUI.drawHelpText(renderer,
int displayIndex = 0; Rect{0, pageHeight - metrics.buttonHintsHeight - metrics.contentSidePadding - 15, pageWidth, 20},
for (size_t i = scrollOffset; i < networks.size() && displayIndex < maxVisibleNetworks; i++, displayIndex++) { tr(STR_NETWORK_LEGEND));
const int networkY = startY + displayIndex * lineHeight;
const auto& network = networks[i];
// Draw selection indicator
if (static_cast<int>(i) == selectedNetworkIndex) {
renderer.drawText(UI_10_FONT_ID, 5, networkY, ">");
}
// Draw network name (truncate if too long)
std::string displayName = network.ssid;
if (displayName.length() > 33) {
displayName.replace(30, displayName.length() - 30, "...");
}
renderer.drawText(UI_10_FONT_ID, 20, networkY, displayName.c_str());
// Draw signal strength indicator
std::string signalStr = getSignalStrengthIndicator(network.rssi);
renderer.drawText(UI_10_FONT_ID, pageWidth - 90, networkY, signalStr.c_str());
// Draw saved indicator (checkmark) for networks with saved passwords
if (network.hasSavedPassword) {
renderer.drawText(UI_10_FONT_ID, pageWidth - 50, networkY, "+");
}
// Draw lock icon for encrypted networks
if (network.isEncrypted) {
renderer.drawText(UI_10_FONT_ID, pageWidth - 30, networkY, "*");
}
}
// Draw scroll indicators if needed
if (scrollOffset > 0) {
renderer.drawText(SMALL_FONT_ID, pageWidth - 15, startY - 10, "^");
}
if (scrollOffset + maxVisibleNetworks < static_cast<int>(networks.size())) {
renderer.drawText(SMALL_FONT_ID, pageWidth - 15, startY + maxVisibleNetworks * lineHeight, "v");
}
// Show network count
char countStr[64];
snprintf(countStr, sizeof(countStr), tr(STR_NETWORKS_FOUND), networks.size());
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 90, countStr);
}
// Show MAC address above the network count and legend
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 105, cachedMacAddress.c_str());
// Draw help text
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, tr(STR_NETWORK_LEGEND));
const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword; const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword;
const char* forgetLabel = hasSavedPassword ? tr(STR_FORGET_BUTTON) : ""; const char* forgetLabel = hasSavedPassword ? tr(STR_FORGET_BUTTON) : "";

View File

@@ -45,7 +45,7 @@ class WifiSelectionActivity final : public ActivityWithSubactivity {
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
WifiSelectionState state = WifiSelectionState::SCANNING; WifiSelectionState state = WifiSelectionState::SCANNING;
int selectedNetworkIndex = 0; size_t selectedNetworkIndex = 0;
std::vector<WifiNetworkInfo> networks; std::vector<WifiNetworkInfo> networks;
const std::function<void(bool connected)> onComplete; const std::function<void(bool connected)> onComplete;

View File

@@ -95,9 +95,6 @@ void ButtonRemapActivity::loop() {
} }
void ButtonRemapActivity::render(Activity::RenderLock&&) { void ButtonRemapActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
const auto labelForHardware = [&](uint8_t hardwareIndex) -> const char* { const auto labelForHardware = [&](uint8_t hardwareIndex) -> const char* {
for (uint8_t i = 0; i < kRoleCount; i++) { for (uint8_t i = 0; i < kRoleCount; i++) {
if (tempMapping[i] == hardwareIndex) { if (tempMapping[i] == hardwareIndex) {
@@ -107,35 +104,41 @@ void ButtonRemapActivity::render(Activity::RenderLock&&) {
return "-"; return "-";
}; };
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_REMAP_FRONT_BUTTONS), true, EpdFontFamily::BOLD); auto metrics = UITheme::getInstance().getMetrics();
renderer.drawCenteredText(UI_10_FONT_ID, 40, tr(STR_REMAP_PROMPT)); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
for (uint8_t i = 0; i < kRoleCount; i++) { renderer.clearScreen();
const int y = 70 + i * 30;
const bool isSelected = (i == currentStep);
// Highlight the role that is currently being assigned. GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_REMAP_FRONT_BUTTONS));
if (isSelected) { GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
renderer.fillRect(0, y - 2, pageWidth - 1, 30); tr(STR_REMAP_PROMPT));
}
const char* roleName = getRoleName(i); int topOffset = metrics.topPadding + metrics.headerHeight + metrics.tabBarHeight + metrics.verticalSpacing;
renderer.drawText(UI_10_FONT_ID, 20, y, roleName, !isSelected); int contentHeight = pageHeight - topOffset - metrics.buttonHintsHeight - metrics.verticalSpacing;
GUI.drawList(
// Show currently assigned hardware button (or unassigned). renderer, Rect{0, topOffset, pageWidth, contentHeight}, kRoleCount, currentStep,
const char* assigned = (tempMapping[i] == kUnassigned) ? tr(STR_UNASSIGNED) : getHardwareName(tempMapping[i]); [&](int index) { return getRoleName(static_cast<uint8_t>(index)); }, nullptr, nullptr,
const auto width = renderer.getTextWidth(UI_10_FONT_ID, assigned); [&](int index) {
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, y, assigned, !isSelected); uint8_t assignedButton = tempMapping[static_cast<uint8_t>(index)];
} return (assignedButton == kUnassigned) ? tr(STR_UNASSIGNED) : getHardwareName(assignedButton);
},
true);
// Temporary warning banner for duplicates. // Temporary warning banner for duplicates.
if (!errorMessage.empty()) { if (!errorMessage.empty()) {
renderer.drawCenteredText(UI_10_FONT_ID, 210, errorMessage.c_str(), true); GUI.drawHelpText(renderer,
Rect{0, pageHeight - metrics.buttonHintsHeight - metrics.contentSidePadding - 15, pageWidth, 20},
errorMessage.c_str());
} }
// Provide side button actions at the bottom of the screen (split across two lines). // Provide side button actions at the bottom of the screen (split across two lines).
renderer.drawCenteredText(SMALL_FONT_ID, 250, tr(STR_REMAP_RESET_HINT), true); GUI.drawHelpText(renderer,
renderer.drawCenteredText(SMALL_FONT_ID, 280, tr(STR_REMAP_CANCEL_HINT), true); Rect{0, topOffset + 4 * metrics.listRowHeight + 4 * metrics.verticalSpacing, pageWidth, 20},
tr(STR_REMAP_RESET_HINT));
GUI.drawHelpText(renderer,
Rect{0, topOffset + 4 * metrics.listRowHeight + 5 * metrics.verticalSpacing + 20, pageWidth, 20},
tr(STR_REMAP_CANCEL_HINT));
// Live preview of logical labels under front buttons. // Live preview of logical labels under front buttons.
// This mirrors the on-device front button order: Back, Confirm, Left, Right. // This mirrors the on-device front button order: Back, Confirm, Left, Right.

View File

@@ -58,7 +58,7 @@ void CalibreSettingsActivity::handleSelection() {
// OPDS Server URL // OPDS Server URL
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), SETTINGS.opdsServerUrl, 10, renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), SETTINGS.opdsServerUrl,
127, // maxLength 127, // maxLength
false, // not password false, // not password
[this](const std::string& url) { [this](const std::string& url) {
@@ -76,7 +76,7 @@ void CalibreSettingsActivity::handleSelection() {
// Username // Username
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_USERNAME), SETTINGS.opdsUsername, 10, renderer, mappedInput, tr(STR_USERNAME), SETTINGS.opdsUsername,
63, // maxLength 63, // maxLength
false, // not password false, // not password
[this](const std::string& username) { [this](const std::string& username) {
@@ -94,7 +94,7 @@ void CalibreSettingsActivity::handleSelection() {
// Password // Password
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_PASSWORD), SETTINGS.opdsPassword, 10, renderer, mappedInput, tr(STR_PASSWORD), SETTINGS.opdsPassword,
63, // maxLength 63, // maxLength
false, // not password mode false, // not password mode
[this](const std::string& password) { [this](const std::string& password) {
@@ -114,42 +114,35 @@ void CalibreSettingsActivity::handleSelection() {
void CalibreSettingsActivity::render(Activity::RenderLock&&) { void CalibreSettingsActivity::render(Activity::RenderLock&&) {
renderer.clearScreen(); renderer.clearScreen();
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_OPDS_BROWSER));
GUI.drawSubHeader(renderer, Rect{0, metrics.topPadding + metrics.headerHeight, pageWidth, metrics.tabBarHeight},
tr(STR_CALIBRE_URL_HINT));
// Draw header const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + metrics.tabBarHeight;
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_OPDS_BROWSER), true, EpdFontFamily::BOLD); const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2;
GUI.drawList(
// Draw info text about Calibre renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(MENU_ITEMS),
renderer.drawCenteredText(UI_10_FONT_ID, 40, tr(STR_CALIBRE_URL_HINT)); static_cast<int>(selectedIndex), [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr,
nullptr,
// Draw selection highlight [this](int index) {
renderer.fillRect(0, 70 + selectedIndex * 30 - 2, pageWidth - 1, 30);
// Draw menu items
for (int i = 0; i < MENU_ITEMS; i++) {
const int settingY = 70 + i * 30;
const bool isSelected = (i == selectedIndex);
renderer.drawText(UI_10_FONT_ID, 20, settingY, I18N.get(menuNames[i]), !isSelected);
// Draw status for each setting // Draw status for each setting
std::string status = std::string("[") + tr(STR_NOT_SET) + "]"; if (index == 0) {
if (i == 0) { return (strlen(SETTINGS.opdsServerUrl) > 0) ? std::string(SETTINGS.opdsServerUrl)
status = (strlen(SETTINGS.opdsServerUrl) > 0) ? std::string("[") + tr(STR_SET) + "]" : std::string(tr(STR_NOT_SET));
: std::string("[") + tr(STR_NOT_SET) + "]"; } else if (index == 1) {
} else if (i == 1) { return (strlen(SETTINGS.opdsUsername) > 0) ? std::string(SETTINGS.opdsUsername)
status = (strlen(SETTINGS.opdsUsername) > 0) ? std::string("[") + tr(STR_SET) + "]" : std::string(tr(STR_NOT_SET));
: std::string("[") + tr(STR_NOT_SET) + "]"; } else if (index == 2) {
} else if (i == 2) { return (strlen(SETTINGS.opdsPassword) > 0) ? std::string("******") : std::string(tr(STR_NOT_SET));
status = (strlen(SETTINGS.opdsPassword) > 0) ? std::string("[") + tr(STR_SET) + "]"
: std::string("[") + tr(STR_NOT_SET) + "]";
}
const auto width = renderer.getTextWidth(UI_10_FONT_ID, status.c_str());
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status.c_str(), !isSelected);
} }
return std::string(tr(STR_NOT_SET));
},
true);
// Draw button hints const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@@ -23,7 +23,7 @@ class CalibreSettingsActivity final : public ActivityWithSubactivity {
private: private:
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
int selectedIndex = 0; size_t selectedIndex = 0;
const std::function<void()> onBack; const std::function<void()> onBack;
void handleSelection(); void handleSelection();
}; };

View File

@@ -19,10 +19,13 @@ void ClearCacheActivity::onEnter() {
void ClearCacheActivity::onExit() { ActivityWithSubactivity::onExit(); } void ClearCacheActivity::onExit() { ActivityWithSubactivity::onExit(); }
void ClearCacheActivity::render(Activity::RenderLock&&) { void ClearCacheActivity::render(Activity::RenderLock&&) {
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_CLEAR_READING_CACHE), true, EpdFontFamily::BOLD);
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_CLEAR_READING_CACHE));
if (state == WARNING) { if (state == WARNING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, tr(STR_CLEAR_CACHE_WARNING_1), true); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, tr(STR_CLEAR_CACHE_WARNING_1), true);
@@ -38,7 +41,7 @@ void ClearCacheActivity::render(Activity::RenderLock&&) {
} }
if (state == CLEARING) { if (state == CLEARING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_CLEARING_CACHE), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, tr(STR_CLEARING_CACHE));
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }

View File

@@ -90,33 +90,28 @@ void KOReaderAuthActivity::onExit() {
void KOReaderAuthActivity::render(Activity::RenderLock&&) { void KOReaderAuthActivity::render(Activity::RenderLock&&) {
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_KOREADER_AUTH), true, EpdFontFamily::BOLD);
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_KOREADER_AUTH));
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2;
if (state == AUTHENTICATING) { if (state == AUTHENTICATING) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, top, statusMessage.c_str());
renderer.displayBuffer(); } else if (state == SUCCESS) {
return; renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_SUCCESS), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, tr(STR_SYNC_READY));
} else if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_AUTH_FAILED), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top + height + 10, errorMessage.c_str());
} }
if (state == SUCCESS) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_AUTH_SUCCESS), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, tr(STR_SYNC_READY));
const auto labels = mappedInput.mapLabels(tr(STR_DONE), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_AUTH_FAILED), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, errorMessage.c_str());
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return;
}
} }
void KOReaderAuthActivity::loop() { void KOReaderAuthActivity::loop() {

View File

@@ -60,7 +60,7 @@ void KOReaderSettingsActivity::handleSelection() {
// Username // Username
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_KOREADER_USERNAME), KOREADER_STORE.getUsername(), 10, renderer, mappedInput, tr(STR_KOREADER_USERNAME), KOREADER_STORE.getUsername(),
64, // maxLength 64, // maxLength
false, // not password false, // not password
[this](const std::string& username) { [this](const std::string& username) {
@@ -77,7 +77,7 @@ void KOReaderSettingsActivity::handleSelection() {
// Password // Password
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_KOREADER_PASSWORD), KOREADER_STORE.getPassword(), 10, renderer, mappedInput, tr(STR_KOREADER_PASSWORD), KOREADER_STORE.getPassword(),
64, // maxLength 64, // maxLength
false, // show characters false, // show characters
[this](const std::string& password) { [this](const std::string& password) {
@@ -96,7 +96,7 @@ void KOReaderSettingsActivity::handleSelection() {
const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl; const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl;
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, 10, renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl,
128, // maxLength - URLs can be long 128, // maxLength - URLs can be long
false, // not password false, // not password
[this](const std::string& url) { [this](const std::string& url) {
@@ -136,44 +136,39 @@ void KOReaderSettingsActivity::handleSelection() {
void KOReaderSettingsActivity::render(Activity::RenderLock&&) { void KOReaderSettingsActivity::render(Activity::RenderLock&&) {
renderer.clearScreen(); renderer.clearScreen();
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
// Draw header GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_KOREADER_SYNC));
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_KOREADER_SYNC), true, EpdFontFamily::BOLD);
// Draw selection highlight const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing;
renderer.fillRect(0, 60 + selectedIndex * 30 - 2, pageWidth - 1, 30); const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2;
GUI.drawList(
// Draw menu items renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast<int>(MENU_ITEMS),
for (int i = 0; i < MENU_ITEMS; i++) { static_cast<int>(selectedIndex), [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr,
const int settingY = 60 + i * 30; nullptr,
const bool isSelected = (i == selectedIndex); [this](int index) {
// Draw status for each setting
renderer.drawText(UI_10_FONT_ID, 20, settingY, I18N.get(menuNames[i]), !isSelected); if (index == 0) {
auto username = KOREADER_STORE.getUsername();
// Draw status for each item return username.empty() ? std::string(tr(STR_NOT_SET)) : username;
std::string status = ""; } else if (index == 1) {
if (i == 0) { return KOREADER_STORE.getPassword().empty() ? std::string(tr(STR_NOT_SET)) : std::string("******");
status = std::string("[") + (KOREADER_STORE.getUsername().empty() ? tr(STR_NOT_SET) : tr(STR_SET)) + "]"; } else if (index == 2) {
} else if (i == 1) { auto serverUrl = KOREADER_STORE.getServerUrl();
status = std::string("[") + (KOREADER_STORE.getPassword().empty() ? tr(STR_NOT_SET) : tr(STR_SET)) + "]"; return serverUrl.empty() ? std::string(tr(STR_DEFAULT_VALUE)) : serverUrl;
} else if (i == 2) { } else if (index == 3) {
status = return KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? std::string(tr(STR_FILENAME))
std::string("[") + (KOREADER_STORE.getServerUrl().empty() ? tr(STR_DEFAULT_VALUE) : tr(STR_CUSTOM)) + "]"; : std::string(tr(STR_BINARY));
} else if (i == 3) { } else if (index == 4) {
status = std::string("[") + return KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]";
(KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? tr(STR_FILENAME) : tr(STR_BINARY)) +
"]";
} else if (i == 4) {
status = KOREADER_STORE.hasCredentials() ? "" : std::string("[") + tr(STR_SET_CREDENTIALS_FIRST) + "]";
} }
return std::string(tr(STR_NOT_SET));
},
true);
const auto width = renderer.getTextWidth(UI_10_FONT_ID, status.c_str()); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status.c_str(), !isSelected);
}
// Draw button hints
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@@ -23,7 +23,7 @@ class KOReaderSettingsActivity final : public ActivityWithSubactivity {
private: private:
ButtonNavigator buttonNavigator; ButtonNavigator buttonNavigator;
int selectedIndex = 0; size_t selectedIndex = 0;
const std::function<void()> onBack; const std::function<void()> onBack;
void handleSelection(); void handleSelection();

View File

@@ -84,6 +84,16 @@ void OtaUpdateActivity::render(Activity::RenderLock&&) {
return; return;
} }
auto metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_UPDATE));
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2;
float updaterProgress = 0; float updaterProgress = 0;
if (state == UPDATE_IN_PROGRESS) { if (state == UPDATE_IN_PROGRESS) {
LOG_DBG("OTA", "Update progress: %d / %d", updater.getProcessedSize(), updater.getTotalSize()); LOG_DBG("OTA", "Update progress: %d / %d", updater.getProcessedSize(), updater.getTotalSize());
@@ -95,60 +105,43 @@ void OtaUpdateActivity::render(Activity::RenderLock&&) {
lastUpdaterPercentage = static_cast<int>(updaterProgress * 100); lastUpdaterPercentage = static_cast<int>(updaterProgress * 100);
} }
const auto pageWidth = renderer.getScreenWidth();
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_UPDATE), true, EpdFontFamily::BOLD);
if (state == CHECKING_FOR_UPDATE) { if (state == CHECKING_FOR_UPDATE) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_CHECKING_UPDATE), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_CHECKING_UPDATE));
renderer.displayBuffer(); } else if (state == WAITING_CONFIRMATION) {
return; renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NEW_UPDATE), true, EpdFontFamily::BOLD);
} renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height + metrics.verticalSpacing,
(std::string(tr(STR_CURRENT_VERSION)) + CROSSPOINT_VERSION).c_str());
if (state == WAITING_CONFIRMATION) { renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, top + height * 2 + metrics.verticalSpacing * 2,
renderer.drawCenteredText(UI_10_FONT_ID, 200, tr(STR_NEW_UPDATE), true, EpdFontFamily::BOLD); (std::string(tr(STR_NEW_VERSION)) + updater.getLatestVersion()).c_str());
renderer.drawText(UI_10_FONT_ID, 20, 250, (std::string(tr(STR_CURRENT_VERSION)) + CROSSPOINT_VERSION).c_str());
renderer.drawText(UI_10_FONT_ID, 20, 270, (std::string(tr(STR_NEW_VERSION)) + updater.getLatestVersion()).c_str());
const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_UPDATE), "", ""); const auto labels = mappedInput.mapLabels(tr(STR_CANCEL), tr(STR_UPDATE), "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); } else if (state == UPDATE_IN_PROGRESS) {
return; renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATING));
}
if (state == UPDATE_IN_PROGRESS) { int y = top + height + metrics.verticalSpacing;
renderer.drawCenteredText(UI_10_FONT_ID, 310, tr(STR_UPDATING), true, EpdFontFamily::BOLD); GUI.drawProgressBar(
renderer.drawRect(20, 350, pageWidth - 40, 50); renderer,
renderer.fillRect(24, 354, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44)), 42); Rect{metrics.contentSidePadding, y, pageWidth - metrics.contentSidePadding * 2, metrics.progressBarHeight},
renderer.drawCenteredText(UI_10_FONT_ID, 420, static_cast<int>(updaterProgress * 100), 100);
y += metrics.progressBarHeight + metrics.verticalSpacing;
renderer.drawCenteredText(UI_10_FONT_ID, y,
(std::to_string(static_cast<int>(updaterProgress * 100)) + "%").c_str()); (std::to_string(static_cast<int>(updaterProgress * 100)) + "%").c_str());
y += height + metrics.verticalSpacing;
renderer.drawCenteredText( renderer.drawCenteredText(
UI_10_FONT_ID, 440, UI_10_FONT_ID, y,
(std::to_string(updater.getProcessedSize()) + " / " + std::to_string(updater.getTotalSize())).c_str()); (std::to_string(updater.getProcessedSize()) + " / " + std::to_string(updater.getTotalSize())).c_str());
renderer.displayBuffer(); } else if (state == NO_UPDATE) {
return; renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_NO_UPDATE), true, EpdFontFamily::BOLD);
} else if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATE_FAILED), true, EpdFontFamily::BOLD);
} else if (state == FINISHED) {
renderer.drawCenteredText(UI_10_FONT_ID, top, tr(STR_UPDATE_COMPLETE), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top + height + metrics.verticalSpacing, tr(STR_POWER_ON_HINT));
} }
if (state == NO_UPDATE) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_NO_UPDATE), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return;
}
if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_UPDATE_FAILED), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}
if (state == FINISHED) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_UPDATE_COMPLETE), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 350, tr(STR_POWER_ON_HINT));
renderer.displayBuffer();
state = SHUTTING_DOWN;
return;
}
} }
void OtaUpdateActivity::loop() { void OtaUpdateActivity::loop() {

View File

@@ -271,7 +271,8 @@ void SettingsActivity::render(Activity::RenderLock&&) {
auto metrics = UITheme::getInstance().getMetrics(); auto metrics = UITheme::getInstance().getMetrics();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_SETTINGS_TITLE)); GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_SETTINGS_TITLE),
CROSSPOINT_VERSION);
std::vector<TabInfo> tabs; std::vector<TabInfo> tabs;
tabs.reserve(categoryCount); tabs.reserve(categoryCount);
@@ -307,12 +308,8 @@ void SettingsActivity::render(Activity::RenderLock&&) {
valueText = std::to_string(SETTINGS.*(setting.valuePtr)); valueText = std::to_string(SETTINGS.*(setting.valuePtr));
} }
return valueText; return valueText;
}); },
true);
// Draw version text
renderer.drawText(SMALL_FONT_ID,
pageWidth - metrics.versionTextRightX - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
metrics.versionTextY, CROSSPOINT_VERSION);
// Draw help text // Draw help text
const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_TOGGLE), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_TOGGLE), tr(STR_DIR_UP), tr(STR_DIR_DOWN));

View File

@@ -42,7 +42,7 @@ int KeyboardEntryActivity::getRowLength(const int row) const {
case 3: case 3:
return 10; // zxcvbnm,./ return 10; // zxcvbnm,./
case 4: case 4:
return 10; // shift (2 wide), space (5 wide), backspace (2 wide), OK return 11; // shift (2 wide), space (5 wide), backspace (2 wide), OK (2 wide)
default: default:
return 0; return 0;
} }
@@ -191,17 +191,19 @@ void KeyboardEntryActivity::loop() {
} }
void KeyboardEntryActivity::render(Activity::RenderLock&&) { void KeyboardEntryActivity::render(Activity::RenderLock&&) {
const auto pageWidth = renderer.getScreenWidth();
renderer.clearScreen(); renderer.clearScreen();
// Draw title const auto pageWidth = renderer.getScreenWidth();
renderer.drawCenteredText(UI_10_FONT_ID, startY, title.c_str()); const auto pageHeight = renderer.getScreenHeight();
auto metrics = UITheme::getInstance().getMetrics();
GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, title.c_str());
// Draw input field // Draw input field
const int inputStartY = startY + 22; const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
int inputEndY = startY + 22; const int inputStartY =
renderer.drawText(UI_10_FONT_ID, 10, inputStartY, "["); metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing + metrics.verticalSpacing * 4;
int inputHeight = 0;
std::string displayText; std::string displayText;
if (isPassword) { if (isPassword) {
@@ -216,34 +218,43 @@ void KeyboardEntryActivity::render(Activity::RenderLock&&) {
// Render input text across multiple lines // Render input text across multiple lines
int lineStartIdx = 0; int lineStartIdx = 0;
int lineEndIdx = displayText.length(); int lineEndIdx = displayText.length();
int textWidth = 0;
while (true) { while (true) {
std::string lineText = displayText.substr(lineStartIdx, lineEndIdx - lineStartIdx); std::string lineText = displayText.substr(lineStartIdx, lineEndIdx - lineStartIdx);
const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, lineText.c_str()); textWidth = renderer.getTextWidth(UI_12_FONT_ID, lineText.c_str());
if (textWidth <= pageWidth - 40) { if (textWidth <= pageWidth - 2 * metrics.contentSidePadding) {
renderer.drawText(UI_10_FONT_ID, 20, inputEndY, lineText.c_str()); if (metrics.keyboardCenteredText) {
renderer.drawCenteredText(UI_12_FONT_ID, inputStartY + inputHeight, lineText.c_str());
} else {
renderer.drawText(UI_12_FONT_ID, metrics.contentSidePadding, inputStartY + inputHeight, lineText.c_str());
}
if (lineEndIdx == displayText.length()) { if (lineEndIdx == displayText.length()) {
break; break;
} }
inputEndY += renderer.getLineHeight(UI_10_FONT_ID); inputHeight += lineHeight;
lineStartIdx = lineEndIdx; lineStartIdx = lineEndIdx;
lineEndIdx = displayText.length(); lineEndIdx = displayText.length();
} else { } else {
lineEndIdx -= 1; lineEndIdx -= 1;
} }
} }
renderer.drawText(UI_10_FONT_ID, pageWidth - 15, inputEndY, "]");
GUI.drawTextField(renderer, Rect{0, inputStartY, pageWidth, inputHeight}, textWidth);
// Draw keyboard - use compact spacing to fit 5 rows on screen // Draw keyboard - use compact spacing to fit 5 rows on screen
const int keyboardStartY = inputEndY + 25; const int keyboardStartY = metrics.keyboardBottomAligned
constexpr int keyWidth = 18; ? pageHeight - metrics.buttonHintsHeight - metrics.verticalSpacing -
constexpr int keyHeight = 18; (metrics.keyboardKeyHeight + metrics.keyboardKeySpacing) * NUM_ROWS
constexpr int keySpacing = 3; : inputStartY + inputHeight + metrics.verticalSpacing * 4;
const int keyWidth = metrics.keyboardKeyWidth;
const int keyHeight = metrics.keyboardKeyHeight;
const int keySpacing = metrics.keyboardKeySpacing;
const char* const* layout = shiftState ? keyboardShift : keyboard; const char* const* layout = shiftState ? keyboardShift : keyboard;
// Calculate left margin to center the longest row (13 keys) // Calculate left margin to center the longest row (13 keys)
constexpr int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing); const int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing);
const int leftMargin = (pageWidth - maxRowWidth) / 2; const int leftMargin = (pageWidth - maxRowWidth) / 2;
for (int row = 0; row < NUM_ROWS; row++) { for (int row = 0; row < NUM_ROWS; row++) {
@@ -253,45 +264,50 @@ void KeyboardEntryActivity::render(Activity::RenderLock&&) {
const int startX = leftMargin; const int startX = leftMargin;
// Handle bottom row (row 4) specially with proper multi-column keys // Handle bottom row (row 4) specially with proper multi-column keys
if (row == 4) { if (row == SPECIAL_ROW) {
// Bottom row layout: SHIFT (2 cols) | SPACE (5 cols) | <- (2 cols) | OK (2 cols) // Bottom row layout: SHIFT (2 cols) | SPACE (5 cols) | <- (2 cols) | OK (2 cols)
// Total: 11 visual columns, but we use logical positions for selection // Total: 11 visual columns, but we use logical positions for selection
int currentX = startX; int currentX = startX;
// SHIFT key (logical col 0, spans 2 key widths) // SHIFT key (logical col 0, spans 2 key widths)
const bool shiftSelected = (selectedRow == 4 && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL); const bool shiftSelected = (selectedRow == SPECIAL_ROW && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL);
static constexpr StrId shiftIds[3] = {StrId::STR_KBD_SHIFT, StrId::STR_KBD_SHIFT_CAPS, StrId::STR_KBD_LOCK}; const int shiftWidth = SPACE_COL - SHIFT_COL;
renderItemWithSelector(currentX + 2, rowY, I18N.get(shiftIds[shiftState]), shiftSelected); const int shiftXWidth = shiftWidth * (keyWidth + keySpacing);
currentX += 2 * (keyWidth + keySpacing); GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, shiftXWidth, keyHeight}, shiftString[shiftState],
shiftSelected);
currentX += shiftXWidth;
// Space bar (logical cols 2-6, spans 5 key widths) // Space bar (logical cols 2-6, spans 5 key widths)
const bool spaceSelected = (selectedRow == 4 && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL); const bool spaceSelected =
const int spaceTextWidth = renderer.getTextWidth(UI_10_FONT_ID, "_____"); (selectedRow == SPECIAL_ROW && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL);
const int spaceXWidth = 5 * (keyWidth + keySpacing); const int spaceWidth = BACKSPACE_COL - SPACE_COL;
const int spaceXPos = currentX + (spaceXWidth - spaceTextWidth) / 2; const int spaceXWidth = spaceWidth * (keyWidth + keySpacing);
renderItemWithSelector(spaceXPos, rowY, "_____", spaceSelected); GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, spaceXWidth, keyHeight}, "_____", spaceSelected);
currentX += spaceXWidth; currentX += spaceXWidth;
// Backspace key (logical col 7, spans 2 key widths) // Backspace key (logical col 7, spans 2 key widths)
const bool bsSelected = (selectedRow == 4 && selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL); const bool bsSelected = (selectedRow == SPECIAL_ROW && selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL);
renderItemWithSelector(currentX + 2, rowY, "<-", bsSelected); const int backspaceWidth = DONE_COL - BACKSPACE_COL;
currentX += 2 * (keyWidth + keySpacing); const int backspaceXWidth = backspaceWidth * (keyWidth + keySpacing);
GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, backspaceXWidth, keyHeight}, "<-", bsSelected);
currentX += backspaceXWidth;
// OK button (logical col 9, spans 2 key widths) // OK button (logical col 9, spans 2 key widths)
const bool okSelected = (selectedRow == 4 && selectedCol >= DONE_COL); const bool okSelected = (selectedRow == SPECIAL_ROW && selectedCol >= DONE_COL);
renderItemWithSelector(currentX + 2, rowY, tr(STR_OK_BUTTON), okSelected); const int okWidth = getRowLength(row) - DONE_COL;
const int okXWidth = okWidth * (keyWidth + keySpacing);
GUI.drawKeyboardKey(renderer, Rect{currentX, rowY, okXWidth, keyHeight}, tr(STR_OK_BUTTON), okSelected);
} else { } else {
// Regular rows: render each key individually // Regular rows: render each key individually
for (int col = 0; col < getRowLength(row); col++) { for (int col = 0; col < getRowLength(row); col++) {
// Get the character to display // Get the character to display
const char c = layout[row][col]; const char c = layout[row][col];
std::string keyLabel(1, c); std::string keyLabel(1, c);
const int charWidth = renderer.getTextWidth(UI_10_FONT_ID, keyLabel.c_str());
const int keyX = startX + col * (keyWidth + keySpacing) + (keyWidth - charWidth) / 2; const int keyX = startX + col * (keyWidth + keySpacing);
const bool isSelected = row == selectedRow && col == selectedCol; const bool isSelected = row == selectedRow && col == selectedCol;
renderItemWithSelector(keyX, rowY, keyLabel.c_str(), isSelected); GUI.drawKeyboardKey(renderer, Rect{keyX, rowY, keyWidth, keyHeight}, keyLabel.c_str(), isSelected);
} }
} }
} }
@@ -301,17 +317,7 @@ void KeyboardEntryActivity::render(Activity::RenderLock&&) {
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
// Draw side button hints for Up/Down navigation // Draw side button hints for Up/Down navigation
GUI.drawSideButtonHints(renderer, tr(STR_DIR_UP), tr(STR_DIR_DOWN)); GUI.drawSideButtonHints(renderer, ">", "<");
renderer.displayBuffer(); renderer.displayBuffer();
} }
void KeyboardEntryActivity::renderItemWithSelector(const int x, const int y, const char* item,
const bool isSelected) const {
if (isSelected) {
const int itemWidth = renderer.getTextWidth(UI_10_FONT_ID, item);
renderer.drawText(UI_10_FONT_ID, x - 6, y, "[");
renderer.drawText(UI_10_FONT_ID, x + itemWidth, y, "]");
}
renderer.drawText(UI_10_FONT_ID, x, y, item);
}

View File

@@ -31,20 +31,18 @@ class KeyboardEntryActivity : public Activity {
* @param mappedInput Reference to MappedInputManager for handling input * @param mappedInput Reference to MappedInputManager for handling input
* @param title Title to display above the keyboard * @param title Title to display above the keyboard
* @param initialText Initial text to show in the input field * @param initialText Initial text to show in the input field
* @param startY Y position to start rendering the keyboard
* @param maxLength Maximum length of input text (0 for unlimited) * @param maxLength Maximum length of input text (0 for unlimited)
* @param isPassword If true, display asterisks instead of actual characters * @param isPassword If true, display asterisks instead of actual characters
* @param onComplete Callback invoked when input is complete * @param onComplete Callback invoked when input is complete
* @param onCancel Callback invoked when input is cancelled * @param onCancel Callback invoked when input is cancelled
*/ */
explicit KeyboardEntryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, explicit KeyboardEntryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
std::string title = "Enter Text", std::string initialText = "", const int startY = 10, std::string title = "Enter Text", std::string initialText = "",
const size_t maxLength = 0, const bool isPassword = false, const size_t maxLength = 0, const bool isPassword = false,
OnCompleteCallback onComplete = nullptr, OnCancelCallback onCancel = nullptr) OnCompleteCallback onComplete = nullptr, OnCancelCallback onCancel = nullptr)
: Activity("KeyboardEntry", renderer, mappedInput), : Activity("KeyboardEntry", renderer, mappedInput),
title(std::move(title)), title(std::move(title)),
text(std::move(initialText)), text(std::move(initialText)),
startY(startY),
maxLength(maxLength), maxLength(maxLength),
isPassword(isPassword), isPassword(isPassword),
onComplete(std::move(onComplete)), onComplete(std::move(onComplete)),
@@ -58,7 +56,6 @@ class KeyboardEntryActivity : public Activity {
private: private:
std::string title; std::string title;
int startY;
std::string text; std::string text;
size_t maxLength; size_t maxLength;
bool isPassword; bool isPassword;
@@ -91,5 +88,4 @@ class KeyboardEntryActivity : public Activity {
char getSelectedChar() const; char getSelectedChar() const;
void handleKeyPress(); void handleKeyPress();
int getRowLength(int row) const; int getRowLength(int row) const;
void renderItemWithSelector(int x, int y, const char* item, bool isSelected) const;
}; };

View File

@@ -7,6 +7,7 @@
#include "RecentBooksStore.h" #include "RecentBooksStore.h"
#include "components/themes/BaseTheme.h" #include "components/themes/BaseTheme.h"
#include "components/themes/lyra/Lyra3CoversTheme.h"
#include "components/themes/lyra/LyraTheme.h" #include "components/themes/lyra/LyraTheme.h"
UITheme UITheme::instance; UITheme UITheme::instance;
@@ -33,6 +34,11 @@ void UITheme::setTheme(CrossPointSettings::UI_THEME type) {
currentTheme = std::make_unique<LyraTheme>(); currentTheme = std::make_unique<LyraTheme>();
currentMetrics = &LyraMetrics::values; currentMetrics = &LyraMetrics::values;
break; break;
case CrossPointSettings::UI_THEME::LYRA_3_COVERS:
LOG_DBG("UI", "Using Lyra 3 Covers theme");
currentTheme = new Lyra3CoversTheme();
currentMetrics = &Lyra3CoversMetrics::values;
break;
} }
} }

View File

@@ -21,6 +21,7 @@ namespace {
constexpr int batteryPercentSpacing = 4; constexpr int batteryPercentSpacing = 4;
constexpr int homeMenuMargin = 20; constexpr int homeMenuMargin = 20;
constexpr int homeMarginTop = 30; constexpr int homeMarginTop = 30;
constexpr int subtitleY = 738;
// Helper: draw battery icon at given position // Helper: draw battery icon at given position
void drawBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, uint16_t percentage) { void drawBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, uint16_t percentage) {
@@ -89,6 +90,7 @@ void BaseTheme::drawProgressBar(const GfxRenderer& renderer, Rect rect, const si
// Use 64-bit arithmetic to avoid overflow for large files // Use 64-bit arithmetic to avoid overflow for large files
const int percent = static_cast<int>((static_cast<uint64_t>(current) * 100) / total); const int percent = static_cast<int>((static_cast<uint64_t>(current) * 100) / total);
LOG_DBG("UI", "Drawing progress bar: current=%u, total=%u, percent=%d", current, total, percent);
// Draw outline // Draw outline
renderer.drawRect(rect.x, rect.y, rect.width, rect.height); renderer.drawRect(rect.x, rect.y, rect.width, rect.height);
@@ -187,7 +189,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
const std::function<std::string(int index)>& rowTitle, const std::function<std::string(int index)>& rowTitle,
const std::function<std::string(int index)>& rowSubtitle, const std::function<std::string(int index)>& rowSubtitle,
const std::function<std::string(int index)>& rowIcon, const std::function<std::string(int index)>& rowIcon,
const std::function<std::string(int index)>& rowValue) const { const std::function<std::string(int index)>& rowValue, bool highlightValue) const {
int rowHeight = int rowHeight =
(rowSubtitle != nullptr) ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight; (rowSubtitle != nullptr) ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight;
int pageItems = rect.height / rowHeight; int pageItems = rect.height / rowHeight;
@@ -253,7 +255,12 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
} }
} }
void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const { void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const {
// Hide last battery draw
constexpr int maxBatteryWidth = 80;
renderer.fillRect(rect.x + rect.width - maxBatteryWidth, rect.y + 5, maxBatteryWidth,
BaseMetrics::values.batteryHeight + 10, false);
const bool showBatteryPercentage = const bool showBatteryPercentage =
SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS; SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS;
// Position icon at right edge, drawBatteryRight will place text to the left // Position icon at right edge, drawBatteryRight will place text to the left
@@ -289,6 +296,36 @@ void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
EpdFontFamily::BOLD); EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_12_FONT_ID, rect.y + 5, truncatedTitle.c_str(), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, rect.y + 5, truncatedTitle.c_str(), true, EpdFontFamily::BOLD);
} }
if (subtitle) {
auto truncatedSubtitle = renderer.truncatedText(
SMALL_FONT_ID, subtitle, rect.width - BaseMetrics::values.contentSidePadding * 2, EpdFontFamily::REGULAR);
int truncatedSubtitleWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedSubtitle.c_str());
renderer.drawText(SMALL_FONT_ID,
rect.x + rect.width - BaseMetrics::values.contentSidePadding - truncatedSubtitleWidth, subtitleY,
truncatedSubtitle.c_str(), true);
}
}
void BaseTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, const char* rightLabel) const {
constexpr int underlineHeight = 2; // Height of selection underline
constexpr int underlineGap = 4; // Gap between text and underline
constexpr int maxListValueWidth = 200;
int currentX = rect.x + BaseMetrics::values.contentSidePadding;
int rightSpace = BaseMetrics::values.contentSidePadding;
if (rightLabel) {
auto truncatedRightLabel =
renderer.truncatedText(SMALL_FONT_ID, rightLabel, maxListValueWidth, EpdFontFamily::REGULAR);
int rightLabelWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedRightLabel.c_str());
renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - BaseMetrics::values.contentSidePadding - rightLabelWidth,
rect.y + 7, truncatedRightLabel.c_str());
rightSpace += rightLabelWidth + 10;
}
auto truncatedLabel = renderer.truncatedText(
UI_12_FONT_ID, label, rect.width - BaseMetrics::values.contentSidePadding - rightSpace, EpdFontFamily::REGULAR);
renderer.drawText(UI_12_FONT_ID, currentX, rect.y, truncatedLabel.c_str(), true, EpdFontFamily::REGULAR);
} }
void BaseTheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const std::vector<TabInfo>& tabs, void BaseTheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const std::vector<TabInfo>& tabs,
@@ -690,3 +727,26 @@ void BaseTheme::drawReadingProgressBar(const GfxRenderer& renderer, const size_t
const int barWidth = progressBarMaxWidth * bookProgress / 100; const int barWidth = progressBarMaxWidth * bookProgress / 100;
renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BaseMetrics::values.bookProgressBarHeight, true); renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BaseMetrics::values.bookProgressBarHeight, true);
} }
void BaseTheme::drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const {
auto metrics = UITheme::getInstance().getMetrics();
auto truncatedLabel =
renderer.truncatedText(SMALL_FONT_ID, label, rect.width - metrics.contentSidePadding * 2, EpdFontFamily::REGULAR);
renderer.drawCenteredText(SMALL_FONT_ID, rect.y, truncatedLabel.c_str());
}
void BaseTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const {
renderer.drawText(UI_12_FONT_ID, rect.x + 10, rect.y, "[");
renderer.drawText(UI_12_FONT_ID, rect.x + rect.width - 15, rect.y + rect.height, "]");
}
void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label,
const bool isSelected) const {
const int itemWidth = renderer.getTextWidth(UI_10_FONT_ID, label);
const int textX = rect.x + (rect.width - itemWidth) / 2;
if (isSelected) {
renderer.drawText(UI_10_FONT_ID, textX - 6, rect.y, "[");
renderer.drawText(UI_10_FONT_ID, textX + itemWidth, rect.y, "]");
}
renderer.drawText(UI_10_FONT_ID, textX, rect.y, label);
}

View File

@@ -51,10 +51,14 @@ struct ThemeMetrics {
int buttonHintsHeight; int buttonHintsHeight;
int sideButtonHintsWidth; int sideButtonHintsWidth;
int versionTextRightX; int progressBarHeight;
int versionTextY;
int bookProgressBarHeight; int bookProgressBarHeight;
int keyboardKeyWidth;
int keyboardKeyHeight;
int keyboardKeySpacing;
bool keyboardBottomAligned;
bool keyboardCenteredText;
}; };
// Default theme implementation (Classic Theme) // Default theme implementation (Classic Theme)
@@ -82,9 +86,13 @@ constexpr ThemeMetrics values = {.batteryWidth = 15,
.homeRecentBooksCount = 1, .homeRecentBooksCount = 1,
.buttonHintsHeight = 40, .buttonHintsHeight = 40,
.sideButtonHintsWidth = 30, .sideButtonHintsWidth = 30,
.versionTextRightX = 20, .progressBarHeight = 16,
.versionTextY = 738, .bookProgressBarHeight = 4,
.bookProgressBarHeight = 4}; .keyboardKeyWidth = 18,
.keyboardKeyHeight = 18,
.keyboardKeySpacing = 3,
.keyboardBottomAligned = false,
.keyboardCenteredText = false};
} }
class BaseTheme { class BaseTheme {
@@ -102,11 +110,14 @@ class BaseTheme {
virtual void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const; virtual void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const;
virtual void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, virtual void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
const std::function<std::string(int index)>& rowTitle, const std::function<std::string(int index)>& rowTitle,
const std::function<std::string(int index)>& rowSubtitle, const std::function<std::string(int index)>& rowSubtitle = nullptr,
const std::function<std::string(int index)>& rowIcon, const std::function<std::string(int index)>& rowIcon = nullptr,
const std::function<std::string(int index)>& rowValue) const; const std::function<std::string(int index)>& rowValue = nullptr,
bool highlightValue = false) const;
virtual void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const; virtual void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title,
const char* subtitle = nullptr) const;
virtual void drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label,
const char* rightLabel = nullptr) const;
virtual void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs, virtual void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs,
bool selected) const; bool selected) const;
virtual void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks, virtual void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
@@ -118,4 +129,7 @@ class BaseTheme {
virtual Rect drawPopup(const GfxRenderer& renderer, const char* message) const; virtual Rect drawPopup(const GfxRenderer& renderer, const char* message) const;
virtual void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const; virtual void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const;
virtual void drawReadingProgressBar(const GfxRenderer& renderer, const size_t bookProgress) const; virtual void drawReadingProgressBar(const GfxRenderer& renderer, const size_t bookProgress) const;
virtual void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const;
virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const;
virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const;
}; };

View File

@@ -0,0 +1,41 @@
#pragma once
#include "components/themes/lyra/LyraTheme.h"
class GfxRenderer;
// Lyra theme metrics (zero runtime cost)
namespace Lyra3CoversMetrics {
constexpr ThemeMetrics values = {.batteryWidth = 16,
.batteryHeight = 12,
.topPadding = 5,
.batteryBarHeight = 40,
.headerHeight = 84,
.verticalSpacing = 16,
.contentSidePadding = 20,
.listRowHeight = 40,
.listWithSubtitleRowHeight = 60,
.menuRowHeight = 64,
.menuSpacing = 8,
.tabSpacing = 8,
.tabBarHeight = 40,
.scrollBarWidth = 4,
.scrollBarRightOffset = 5,
.homeTopPadding = 56,
.homeCoverHeight = 226,
.homeCoverTileHeight = 287,
.homeRecentBooksCount = 3,
.buttonHintsHeight = 40,
.sideButtonHintsWidth = 30,
.progressBarHeight = 16,
.bookProgressBarHeight = 4};
}
class Lyra3CoversTheme : public LyraTheme {
public:
void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
const int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored,
std::function<bool()> storeCoverBuffer) const override;
};

View File

@@ -23,6 +23,10 @@ constexpr int batteryPercentSpacing = 4;
constexpr int hPaddingInSelection = 8; constexpr int hPaddingInSelection = 8;
constexpr int cornerRadius = 6; constexpr int cornerRadius = 6;
constexpr int topHintButtonY = 345; constexpr int topHintButtonY = 345;
constexpr int popupMarginX = 16;
constexpr int popupMarginY = 12;
constexpr int maxSubtitleWidth = 100;
constexpr int maxListValueWidth = 200;
} // namespace } // namespace
void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const { void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
@@ -104,7 +108,7 @@ void LyraTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const b
} }
} }
void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const { void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const {
renderer.fillRect(rect.x, rect.y, rect.width, rect.height, false); renderer.fillRect(rect.x, rect.y, rect.width, rect.height, false);
const bool showBatteryPercentage = const bool showBatteryPercentage =
@@ -135,14 +139,43 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
} }
} }
int maxTitleWidth =
rect.width - LyraMetrics::values.contentSidePadding * 2 - (subtitle != nullptr ? maxSubtitleWidth : 0);
if (title) { if (title) {
auto truncatedTitle = renderer.truncatedText( auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title, maxTitleWidth, EpdFontFamily::BOLD);
UI_12_FONT_ID, title, rect.width - LyraMetrics::values.contentSidePadding * 2, EpdFontFamily::BOLD);
renderer.drawText(UI_12_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding, renderer.drawText(UI_12_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding,
rect.y + LyraMetrics::values.batteryBarHeight + 3, truncatedTitle.c_str(), true, rect.y + LyraMetrics::values.batteryBarHeight + 3, truncatedTitle.c_str(), true,
EpdFontFamily::BOLD); EpdFontFamily::BOLD);
renderer.drawLine(rect.x, rect.y + rect.height - 3, rect.x + rect.width, rect.y + rect.height - 3, 3, true); renderer.drawLine(rect.x, rect.y + rect.height - 3, rect.x + rect.width, rect.y + rect.height - 3, 3, true);
} }
if (subtitle) {
auto truncatedSubtitle = renderer.truncatedText(SMALL_FONT_ID, subtitle, maxSubtitleWidth, EpdFontFamily::REGULAR);
int truncatedSubtitleWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedSubtitle.c_str());
renderer.drawText(SMALL_FONT_ID,
rect.x + rect.width - LyraMetrics::values.contentSidePadding - truncatedSubtitleWidth,
rect.y + 50, truncatedSubtitle.c_str(), true);
}
}
void LyraTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, const char* rightLabel) const {
int currentX = rect.x + LyraMetrics::values.contentSidePadding;
int rightSpace = LyraMetrics::values.contentSidePadding;
if (rightLabel) {
auto truncatedRightLabel =
renderer.truncatedText(SMALL_FONT_ID, rightLabel, maxListValueWidth, EpdFontFamily::REGULAR);
int rightLabelWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedRightLabel.c_str());
renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - LyraMetrics::values.contentSidePadding - rightLabelWidth,
rect.y + 7, truncatedRightLabel.c_str());
rightSpace += rightLabelWidth + hPaddingInSelection;
}
auto truncatedLabel = renderer.truncatedText(
UI_10_FONT_ID, label, rect.width - LyraMetrics::values.contentSidePadding - rightSpace, EpdFontFamily::REGULAR);
renderer.drawText(UI_10_FONT_ID, currentX, rect.y + 6, truncatedLabel.c_str(), true, EpdFontFamily::REGULAR);
renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width, rect.y + rect.height - 1, true);
} }
void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs, void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs,
@@ -181,7 +214,7 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
const std::function<std::string(int index)>& rowTitle, const std::function<std::string(int index)>& rowTitle,
const std::function<std::string(int index)>& rowSubtitle, const std::function<std::string(int index)>& rowSubtitle,
const std::function<std::string(int index)>& rowIcon, const std::function<std::string(int index)>& rowIcon,
const std::function<std::string(int index)>& rowValue) const { const std::function<std::string(int index)>& rowValue, bool highlightValue) const {
int rowHeight = int rowHeight =
(rowSubtitle != nullptr) ? LyraMetrics::values.listWithSubtitleRowHeight : LyraMetrics::values.listRowHeight; (rowSubtitle != nullptr) ? LyraMetrics::values.listWithSubtitleRowHeight : LyraMetrics::values.listRowHeight;
int pageItems = rect.height / rowHeight; int pageItems = rect.height / rowHeight;
@@ -216,8 +249,14 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
const int itemY = rect.y + (i % pageItems) * rowHeight; const int itemY = rect.y + (i % pageItems) * rowHeight;
// Draw name // Draw name
int textWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2 - int valueWidth = 0;
(rowValue != nullptr ? 60 : 0); // TODO truncate according to value width? std::string valueText = "";
if (rowValue != nullptr) {
valueText = rowValue(i);
valueText = renderer.truncatedText(UI_10_FONT_ID, valueText.c_str(), maxListValueWidth);
valueWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()) + hPaddingInSelection;
}
int textWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2 - valueWidth;
auto itemName = rowTitle(i); auto itemName = rowTitle(i);
auto item = renderer.truncatedText(UI_10_FONT_ID, itemName.c_str(), textWidth); auto item = renderer.truncatedText(UI_10_FONT_ID, itemName.c_str(), textWidth);
renderer.drawText(UI_10_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection * 2, renderer.drawText(UI_10_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection * 2,
@@ -231,22 +270,16 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
itemY + 30, subtitle.c_str(), true); itemY + 30, subtitle.c_str(), true);
} }
if (rowValue != nullptr) {
// Draw value // Draw value
std::string valueText = rowValue(i);
if (!valueText.empty()) { if (!valueText.empty()) {
const auto valueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, valueText.c_str()); if (i == selectedIndex && highlightValue) {
if (i == selectedIndex) {
renderer.fillRoundedRect( renderer.fillRoundedRect(
contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection * 2 - valueTextWidth, itemY, contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - valueWidth, itemY,
valueTextWidth + hPaddingInSelection * 2, rowHeight, cornerRadius, Color::Black); valueWidth + hPaddingInSelection, rowHeight, cornerRadius, Color::Black);
} }
renderer.drawText(UI_10_FONT_ID, renderer.drawText(UI_10_FONT_ID, rect.x + contentWidth - LyraMetrics::values.contentSidePadding - valueWidth,
contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - valueTextWidth, itemY + 6, valueText.c_str(), !(i == selectedIndex && highlightValue));
itemY + 6, valueText.c_str(), i != selectedIndex);
}
} }
} }
} }
@@ -331,12 +364,10 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
const int coverHeight = LyraMetrics::values.homeCoverHeight; const int coverHeight = LyraMetrics::values.homeCoverHeight;
if (bookCount == 0) { if (bookCount == 0) {
const int centerY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2; drawEmptyRecents(renderer, rect);
renderer.drawCenteredText(UI_12_FONT_ID, centerY, tr(STR_CHOOSE_SOMETHING), true);
return; return;
} }
// Word-wrap helper: splits text into lines fitting maxWidth, capped at maxLines with ellipsis
auto wrapText = [&renderer](int fontId, const std::string& text, int maxWidth, auto wrapText = [&renderer](int fontId, const std::string& text, int maxWidth,
int maxLines) -> std::vector<std::string> { int maxLines) -> std::vector<std::string> {
std::vector<std::string> words; std::vector<std::string> words;
@@ -390,8 +421,6 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
return lines; return lines;
}; };
// Cover rendering helper: draws bitmap maintaining aspect ratio within a slot.
// Crops if wider than slot, centers if narrower. Returns actual rendered width.
auto& storage = HalStorage::getInstance(); auto& storage = HalStorage::getInstance();
auto renderCoverBitmap = [&renderer, &storage, coverHeight](const std::string& coverBmpPath, int slotX, int slotY, auto renderCoverBitmap = [&renderer, &storage, coverHeight](const std::string& coverBmpPath, int slotX, int slotY,
int slotWidth) { int slotWidth) {
@@ -418,11 +447,9 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
}; };
if (bookCount == 1) { if (bookCount == 1) {
// ===== SINGLE BOOK: HORIZONTAL LAYOUT (cover left, text right) =====
const bool bookSelected = (selectorIndex == 0); const bool bookSelected = (selectorIndex == 0);
const int cardX = LyraMetrics::values.contentSidePadding; const int cardX = LyraMetrics::values.contentSidePadding;
const int cardWidth = rect.width - 2 * LyraMetrics::values.contentSidePadding; const int cardWidth = rect.width - 2 * LyraMetrics::values.contentSidePadding;
// Fixed cover slot width based on typical book aspect ratio (~0.65)
const int coverSlotWidth = static_cast<int>(coverHeight * 0.65f); const int coverSlotWidth = static_cast<int>(coverHeight * 0.65f);
const int textGap = hPaddingInSelection * 2; const int textGap = hPaddingInSelection * 2;
const int textAreaX = cardX + hPaddingInSelection + coverSlotWidth + textGap; const int textAreaX = cardX + hPaddingInSelection + coverSlotWidth + textGap;
@@ -439,20 +466,14 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
coverRendered = true; coverRendered = true;
} }
// Selection highlight: border strips around the cover, fill the text area
if (bookSelected) { if (bookSelected) {
// Top strip
renderer.fillRoundedRect(cardX, tileY, cardWidth, hPaddingInSelection, cornerRadius, true, true, false, false, renderer.fillRoundedRect(cardX, tileY, cardWidth, hPaddingInSelection, cornerRadius, true, true, false, false,
Color::LightGray); Color::LightGray);
// Left strip (alongside cover)
renderer.fillRectDither(cardX, tileY + hPaddingInSelection, hPaddingInSelection, coverHeight, Color::LightGray); renderer.fillRectDither(cardX, tileY + hPaddingInSelection, hPaddingInSelection, coverHeight, Color::LightGray);
// Right strip
renderer.fillRectDither(cardX + cardWidth - hPaddingInSelection, tileY + hPaddingInSelection, hPaddingInSelection, renderer.fillRectDither(cardX + cardWidth - hPaddingInSelection, tileY + hPaddingInSelection, hPaddingInSelection,
coverHeight, Color::LightGray); coverHeight, Color::LightGray);
// Text area background (right of cover, alongside cover height)
renderer.fillRectDither(cardX + hPaddingInSelection + coverSlotWidth, tileY + hPaddingInSelection, renderer.fillRectDither(cardX + hPaddingInSelection + coverSlotWidth, tileY + hPaddingInSelection,
cardWidth - hPaddingInSelection * 2 - coverSlotWidth, coverHeight, Color::LightGray); cardWidth - hPaddingInSelection * 2 - coverSlotWidth, coverHeight, Color::LightGray);
// Bottom strip (below cover, full width)
const int bottomY = tileY + hPaddingInSelection + coverHeight; const int bottomY = tileY + hPaddingInSelection + coverHeight;
const int bottomH = tileHeight - hPaddingInSelection - coverHeight; const int bottomH = tileHeight - hPaddingInSelection - coverHeight;
if (bottomH > 0) { if (bottomH > 0) {
@@ -461,7 +482,6 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
} }
} }
// Title: UI_12 font, wrap generously (up to 5 lines)
auto titleLines = wrapText(UI_12_FONT_ID, recentBooks[0].title, textAreaWidth, 5); auto titleLines = wrapText(UI_12_FONT_ID, recentBooks[0].title, textAreaWidth, 5);
const int titleLineHeight = renderer.getLineHeight(UI_12_FONT_ID); const int titleLineHeight = renderer.getLineHeight(UI_12_FONT_ID);
int textY = tileY + hPaddingInSelection + 3; int textY = tileY + hPaddingInSelection + 3;
@@ -470,7 +490,6 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
textY += titleLineHeight; textY += titleLineHeight;
} }
// Author: UI_10 font
if (!recentBooks[0].author.empty()) { if (!recentBooks[0].author.empty()) {
textY += 4; textY += 4;
auto author = renderer.truncatedText(UI_10_FONT_ID, recentBooks[0].author.c_str(), textAreaWidth); auto author = renderer.truncatedText(UI_10_FONT_ID, recentBooks[0].author.c_str(), textAreaWidth);
@@ -478,12 +497,9 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
} }
} else { } else {
// ===== MULTI BOOK: TILE LAYOUT (2-3 books) =====
const int tileWidth = (rect.width - 2 * LyraMetrics::values.contentSidePadding) / bookCount; const int tileWidth = (rect.width - 2 * LyraMetrics::values.contentSidePadding) / bookCount;
// Bottom section height: everything below cover + top padding
const int bottomSectionHeight = tileHeight - coverHeight - hPaddingInSelection; const int bottomSectionHeight = tileHeight - coverHeight - hPaddingInSelection;
// Render covers (first render only)
if (!coverRendered) { if (!coverRendered) {
for (int i = 0; i < bookCount; i++) { for (int i = 0; i < bookCount; i++) {
int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i; int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i;
@@ -498,27 +514,22 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
coverRendered = true; coverRendered = true;
} }
// Draw selection and text for each book tile
for (int i = 0; i < bookCount; i++) { for (int i = 0; i < bookCount; i++) {
bool bookSelected = (selectorIndex == i); bool bookSelected = (selectorIndex == i);
int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i; int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i;
const int maxTextWidth = tileWidth - 2 * hPaddingInSelection; const int maxTextWidth = tileWidth - 2 * hPaddingInSelection;
if (bookSelected) { if (bookSelected) {
// Top strip
renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false, renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false,
Color::LightGray); Color::LightGray);
// Left/right strips alongside cover
renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection, coverHeight, renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection, coverHeight,
Color::LightGray); Color::LightGray);
renderer.fillRectDither(tileX + tileWidth - hPaddingInSelection, tileY + hPaddingInSelection, renderer.fillRectDither(tileX + tileWidth - hPaddingInSelection, tileY + hPaddingInSelection,
hPaddingInSelection, coverHeight, Color::LightGray); hPaddingInSelection, coverHeight, Color::LightGray);
// Bottom section: spans from below cover to the card bottom
renderer.fillRoundedRect(tileX, tileY + coverHeight + hPaddingInSelection, tileWidth, bottomSectionHeight, renderer.fillRoundedRect(tileX, tileY + coverHeight + hPaddingInSelection, tileWidth, bottomSectionHeight,
cornerRadius, false, false, true, true, Color::LightGray); cornerRadius, false, false, true, true, Color::LightGray);
} }
// Word-wrap title to 2 lines (UI_10)
auto titleLines = wrapText(UI_10_FONT_ID, recentBooks[i].title, maxTextWidth, 2); auto titleLines = wrapText(UI_10_FONT_ID, recentBooks[i].title, maxTextWidth, 2);
const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID); const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
@@ -528,7 +539,6 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
textY += lineHeight; textY += lineHeight;
} }
// Author below title
if (!recentBooks[i].author.empty()) { if (!recentBooks[i].author.empty()) {
auto author = renderer.truncatedText(SMALL_FONT_ID, recentBooks[i].author.c_str(), maxTextWidth); auto author = renderer.truncatedText(SMALL_FONT_ID, recentBooks[i].author.c_str(), maxTextWidth);
renderer.drawText(SMALL_FONT_ID, tileX + hPaddingInSelection, textY + 2, author.c_str(), true); renderer.drawText(SMALL_FONT_ID, tileX + hPaddingInSelection, textY + 2, author.c_str(), true);
@@ -537,15 +547,22 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
} }
} }
void LyraTheme::drawEmptyRecents(const GfxRenderer& renderer, const Rect rect) const {
constexpr int padding = 48;
renderer.drawText(UI_12_FONT_ID, rect.x + padding,
rect.y + rect.height / 2 - renderer.getLineHeight(UI_12_FONT_ID) - 2, tr(STR_NO_OPEN_BOOK), true,
EpdFontFamily::BOLD);
renderer.drawText(UI_10_FONT_ID, rect.x + padding, rect.y + rect.height / 2 + 2, tr(STR_START_READING), true);
}
void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex, void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex,
const std::function<std::string(int index)>& buttonLabel, const std::function<std::string(int index)>& buttonLabel,
const std::function<std::string(int index)>& rowIcon) const { const std::function<std::string(int index)>& rowIcon) const {
for (int i = 0; i < buttonCount; ++i) { for (int i = 0; i < buttonCount; ++i) {
int tileWidth = (rect.width - LyraMetrics::values.contentSidePadding * 2 - LyraMetrics::values.menuSpacing) / 2; int tileWidth = rect.width - LyraMetrics::values.contentSidePadding * 2;
Rect tileRect = Rect tileRect = Rect{rect.x + LyraMetrics::values.contentSidePadding,
Rect{rect.x + LyraMetrics::values.contentSidePadding + (LyraMetrics::values.menuSpacing + tileWidth) * (i % 2), rect.y + i * (LyraMetrics::values.menuRowHeight + LyraMetrics::values.menuSpacing), tileWidth,
rect.y + static_cast<int>(i / 2) * (LyraMetrics::values.menuRowHeight + LyraMetrics::values.menuSpacing), LyraMetrics::values.menuRowHeight};
tileWidth, LyraMetrics::values.menuRowHeight};
const bool selected = selectedIndex == i; const bool selected = selectedIndex == i;
@@ -582,3 +599,35 @@ Rect LyraTheme::drawPopup(const GfxRenderer& renderer, const char* message) cons
renderer.displayBuffer(); renderer.displayBuffer();
return Rect{x, y, w, h}; return Rect{x, y, w, h};
} }
void LyraTheme::fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const {
constexpr int barHeight = 4;
const int barWidth = layout.width - popupMarginX * 2;
const int barX = layout.x + (layout.width - barWidth) / 2;
const int barY = layout.y + layout.height - popupMarginY / 2 - barHeight / 2 - 1;
int fillWidth = barWidth * progress / 100;
renderer.fillRect(barX, barY, fillWidth, barHeight, false);
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
}
void LyraTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const {
int lineY = rect.y + rect.height + renderer.getLineHeight(UI_12_FONT_ID) + LyraMetrics::values.verticalSpacing;
int lineW = textWidth + hPaddingInSelection * 2;
renderer.drawLine(rect.x + (rect.width - lineW) / 2, lineY, rect.x + (rect.width + lineW) / 2, lineY, 3);
}
void LyraTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label,
const bool isSelected) const {
if (isSelected) {
renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cornerRadius, Color::Black);
}
const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, label);
const int textX = rect.x + (rect.width - textWidth) / 2;
const int textY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2;
renderer.drawText(UI_12_FONT_ID, textX, textY, label, !isSelected);
}

View File

@@ -27,9 +27,13 @@ constexpr ThemeMetrics values = {.batteryWidth = 16,
.homeRecentBooksCount = 3, .homeRecentBooksCount = 3,
.buttonHintsHeight = 40, .buttonHintsHeight = 40,
.sideButtonHintsWidth = 30, .sideButtonHintsWidth = 30,
.versionTextRightX = 20, .progressBarHeight = 16,
.versionTextY = 55, .bookProgressBarHeight = 4,
.bookProgressBarHeight = 4}; .keyboardKeyWidth = 31,
.keyboardKeyHeight = 50,
.keyboardKeySpacing = 0,
.keyboardBottomAligned = true,
.keyboardCenteredText = true};
} }
class LyraTheme : public BaseTheme { class LyraTheme : public BaseTheme {
@@ -38,14 +42,16 @@ class LyraTheme : public BaseTheme {
// void drawProgressBar(const GfxRenderer& renderer, Rect rect, size_t current, size_t total) override; // void drawProgressBar(const GfxRenderer& renderer, Rect rect, size_t current, size_t total) override;
void drawBatteryLeft(const GfxRenderer& renderer, Rect rect, bool showPercentage = true) const override; void drawBatteryLeft(const GfxRenderer& renderer, Rect rect, bool showPercentage = true) const override;
void drawBatteryRight(const GfxRenderer& renderer, Rect rect, bool showPercentage = true) const override; void drawBatteryRight(const GfxRenderer& renderer, Rect rect, bool showPercentage = true) const override;
void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title) const override; void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const override;
void drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label,
const char* rightLabel = nullptr) const override;
void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs, void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs,
bool selected) const override; bool selected) const override;
void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex, void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
const std::function<std::string(int index)>& rowTitle, const std::function<std::string(int index)>& rowTitle,
const std::function<std::string(int index)>& rowSubtitle, const std::function<std::string(int index)>& rowSubtitle,
const std::function<std::string(int index)>& rowIcon, const std::function<std::string(int index)>& rowIcon,
const std::function<std::string(int index)>& rowValue) const override; const std::function<std::string(int index)>& rowValue, bool highlightValue) const override;
void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3,
const char* btn4) const override; const char* btn4) const override;
void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const override; void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const override;
@@ -55,5 +61,9 @@ class LyraTheme : public BaseTheme {
void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks, void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
const int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored, const int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored,
std::function<bool()> storeCoverBuffer) const override; std::function<bool()> storeCoverBuffer) const override;
void drawEmptyRecents(const GfxRenderer& renderer, const Rect rect) const;
Rect drawPopup(const GfxRenderer& renderer, const char* message) const override; Rect drawPopup(const GfxRenderer& renderer, const char* message) const override;
void fillPopupProgress(const GfxRenderer& renderer, const Rect& layout, const int progress) const override;
void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const override;
void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const override;
}; };