From 077d134ac8820ffd6d75f193566ee1112fb632d2 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 5 Apr 2026 22:44:05 +0200 Subject: [PATCH 1/2] Adjust menu design to still fit on screen --- src/activities/home/HomeActivity.cpp | 99 +++++++++++++++++-- src/components/themes/BaseTheme.cpp | 26 +++-- .../themes/lyra/Lyra3CoversTheme.cpp | 42 ++++---- src/components/themes/lyra/LyraTheme.cpp | 51 ++++++---- 4 files changed, 163 insertions(+), 55 deletions(-) diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index c9057e50..59080969 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -19,6 +19,85 @@ #include "components/UITheme.h" #include "fontIds.h" +namespace { +constexpr int CLASSIC_MIN_RECENT_TILE_HEIGHT = 280; +constexpr int LYRA_MIN_RECENT_TILE_HEIGHT = 170; +constexpr int LYRA_3_COVERS_MIN_RECENT_TILE_HEIGHT = 200; +constexpr int CLASSIC_MIN_RECENT_TO_MENU_GAP = 2; +constexpr int LYRA_MIN_RECENT_TO_MENU_GAP = 4; + +struct HomeScreenLayout { + int recentTileHeight; + int recentToMenuGap; + int menuHeight; +}; + +bool isLyraFamilyTheme() { + const auto theme = static_cast(SETTINGS.uiTheme); + return theme == CrossPointSettings::UI_THEME::LYRA || theme == CrossPointSettings::UI_THEME::LYRA_3_COVERS; +} + +bool isLyraExtendedTheme() { + return static_cast(SETTINGS.uiTheme) == CrossPointSettings::UI_THEME::LYRA_3_COVERS; +} + +int getMinRecentTileHeight() { + if (isLyraExtendedTheme()) { + return LYRA_3_COVERS_MIN_RECENT_TILE_HEIGHT; + } + if (isLyraFamilyTheme()) { + return LYRA_MIN_RECENT_TILE_HEIGHT; + } + return CLASSIC_MIN_RECENT_TILE_HEIGHT; +} + +int getMinRecentToMenuGap() { + return isLyraFamilyTheme() ? LYRA_MIN_RECENT_TO_MENU_GAP : CLASSIC_MIN_RECENT_TO_MENU_GAP; +} + +HomeScreenLayout computeHomeScreenLayout(const ThemeMetrics& metrics, int contentHeight, int menuItemCount) { + HomeScreenLayout layout{metrics.homeCoverTileHeight, metrics.verticalSpacing, 0}; + + const int menuRequiredHeight = + menuItemCount * metrics.menuRowHeight + std::max(0, menuItemCount - 1) * metrics.menuSpacing; + + auto computeMenuHeight = [&]() { + return contentHeight - (metrics.homeTopPadding + layout.recentTileHeight + layout.recentToMenuGap); + }; + + layout.menuHeight = computeMenuHeight(); + if (layout.menuHeight >= menuRequiredHeight) { + return layout; + } + + const int gapReduction = + std::min(layout.recentToMenuGap - getMinRecentToMenuGap(), menuRequiredHeight - layout.menuHeight); + if (gapReduction > 0) { + layout.recentToMenuGap -= gapReduction; + layout.menuHeight = computeMenuHeight(); + } + + if (layout.menuHeight >= menuRequiredHeight) { + return layout; + } + + const int tileReduction = + std::min(layout.recentTileHeight - getMinRecentTileHeight(), menuRequiredHeight - layout.menuHeight); + if (tileReduction > 0) { + layout.recentTileHeight -= tileReduction; + layout.menuHeight = computeMenuHeight(); + } + + layout.menuHeight = std::max(0, layout.menuHeight); + return layout; +} + +int getHomeCoverRenderHeight(const HomeScreenLayout& layout) { + return isLyraExtendedTheme() ? std::max(120, layout.recentTileHeight - 58) + : std::max(120, layout.recentTileHeight - (isLyraFamilyTheme() ? 16 : 0)); +} +} // namespace + int HomeActivity::getMenuItemCount() const { int count = 5; // File Browser, Recents, File transfer, Weather, Settings if (!recentBooks.empty()) { @@ -222,11 +301,6 @@ void HomeActivity::render(RenderLock&&) { GUI.drawHeader(renderer, Rect{contentRect.x, metrics.topPadding, contentRect.width, metrics.homeTopPadding}, nullptr); - GUI.drawRecentBookCover(renderer, - Rect{contentRect.x, metrics.homeTopPadding, contentRect.width, metrics.homeCoverTileHeight}, - recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored, - std::bind(&HomeActivity::storeCoverBuffer, this)); - // Build menu items dynamically std::vector menuItems = {tr(STR_BROWSE_FILES), tr(STR_MENU_RECENT_BOOKS), tr(STR_FILE_TRANSFER), tr(STR_WEATHER), tr(STR_SETTINGS_TITLE)}; @@ -238,11 +312,18 @@ void HomeActivity::render(RenderLock&&) { menuIcons.insert(menuIcons.begin() + 2, Library); } + const HomeScreenLayout layout = + computeHomeScreenLayout(metrics, contentRect.height, static_cast(menuItems.size())); + + GUI.drawRecentBookCover(renderer, + Rect{contentRect.x, metrics.homeTopPadding, contentRect.width, layout.recentTileHeight}, + recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored, + std::bind(&HomeActivity::storeCoverBuffer, this)); + GUI.drawButtonMenu( renderer, - Rect{contentRect.x, metrics.homeTopPadding + metrics.homeCoverTileHeight + metrics.verticalSpacing, - contentRect.width, - contentRect.height - (metrics.headerHeight + metrics.homeTopPadding + metrics.verticalSpacing * 2)}, + Rect{contentRect.x, metrics.homeTopPadding + layout.recentTileHeight + layout.recentToMenuGap, contentRect.width, + layout.menuHeight}, static_cast(menuItems.size()), selectorIndex - recentBooks.size(), [&menuItems](int index) { return std::string(menuItems[index]); }, [&menuIcons](int index) { return menuIcons[index]; }); @@ -257,7 +338,7 @@ void HomeActivity::render(RenderLock&&) { requestUpdate(); } else if (!recentsLoaded && !recentsLoading) { recentsLoading = true; - loadRecentCovers(metrics.homeCoverHeight); + loadRecentCovers(getHomeCoverRenderHeight(layout)); } } diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 53508278..c982982b 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -652,18 +652,33 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: void BaseTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex, const std::function& buttonLabel, const std::function& rowIcon) const { + int rowHeight = BaseMetrics::values.menuRowHeight; + int rowSpacing = BaseMetrics::values.menuSpacing; + if (buttonCount > 0 && rect.height > 0) { + const int defaultHeight = buttonCount * rowHeight + std::max(0, buttonCount - 1) * rowSpacing; + if (defaultHeight > rect.height) { + rowSpacing = std::max(0, (rect.height - buttonCount * rowHeight) / std::max(1, buttonCount - 1)); + if (buttonCount * rowHeight + std::max(0, buttonCount - 1) * rowSpacing > rect.height) { + rowHeight = std::max(30, (rect.height - std::max(0, buttonCount - 1) * rowSpacing) / buttonCount); + } + if (buttonCount * rowHeight + std::max(0, buttonCount - 1) * rowSpacing > rect.height) { + rowHeight = std::max(1, rect.height / buttonCount); + rowSpacing = 0; + } + } + } + for (int i = 0; i < buttonCount; ++i) { - const int tileY = BaseMetrics::values.verticalSpacing + rect.y + - static_cast(i) * (BaseMetrics::values.menuRowHeight + BaseMetrics::values.menuSpacing); + const int tileY = rect.y + static_cast(i) * (rowHeight + rowSpacing); const bool selected = selectedIndex == i; if (selected) { renderer.fillRect(rect.x + BaseMetrics::values.contentSidePadding, tileY, - rect.width - BaseMetrics::values.contentSidePadding * 2, BaseMetrics::values.menuRowHeight); + rect.width - BaseMetrics::values.contentSidePadding * 2, rowHeight); } else { renderer.drawRect(rect.x + BaseMetrics::values.contentSidePadding, tileY, - rect.width - BaseMetrics::values.contentSidePadding * 2, BaseMetrics::values.menuRowHeight); + rect.width - BaseMetrics::values.contentSidePadding * 2, rowHeight); } std::string labelStr = buttonLabel(i); @@ -671,8 +686,7 @@ void BaseTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, label); const int textX = rect.x + (rect.width - textWidth) / 2; const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID); - const int textY = - tileY + (BaseMetrics::values.menuRowHeight - lineHeight) / 2; // vertically centered assuming y is top of text + const int textY = tileY + (rowHeight - lineHeight) / 2; // vertically centered assuming y is top of text // Invert text when the tile is selected, to contrast with the filled background renderer.drawText(UI_10_FONT_ID, textX, textY, label, selectedIndex != i); } diff --git a/src/components/themes/lyra/Lyra3CoversTheme.cpp b/src/components/themes/lyra/Lyra3CoversTheme.cpp index 68d8b234..5e568174 100644 --- a/src/components/themes/lyra/Lyra3CoversTheme.cpp +++ b/src/components/themes/lyra/Lyra3CoversTheme.cpp @@ -23,6 +23,8 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con bool& bufferRestored, std::function storeCoverBuffer) const { const int tileWidth = (rect.width - 2 * Lyra3CoversMetrics::values.contentSidePadding) / 3; const int tileY = rect.y; + const int titleAreaHeight = renderer.getLineHeight(SMALL_FONT_ID) * 3 + hPaddingInSelection + 5; + const int coverHeight = std::max(120, rect.height - titleAreaHeight); const bool hasContinueReading = !recentBooks.empty(); // Draw book card regardless, fill with message based on `hasContinueReading` @@ -38,40 +40,38 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con if (coverPath.empty()) { hasCover = false; } else { - const std::string coverBmpPath = - UITheme::getCoverThumbPath(coverPath, Lyra3CoversMetrics::values.homeCoverHeight); + const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, coverHeight); // First time: load cover from SD and render FsFile file; if (Storage.openFileForRead("HOME", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { - float coverHeight = static_cast(bitmap.getHeight()); - float coverWidth = static_cast(bitmap.getWidth()); - float ratio = coverWidth / coverHeight; - const float tileRatio = static_cast(tileWidth - 2 * hPaddingInSelection) / - static_cast(Lyra3CoversMetrics::values.homeCoverHeight); - float cropX = 1.0f - (tileRatio / ratio); + const float bitmapHeight = static_cast(bitmap.getHeight()); + const float bitmapWidth = static_cast(bitmap.getWidth()); + const float ratio = bitmapWidth / bitmapHeight; + const float tileRatio = + static_cast(tileWidth - 2 * hPaddingInSelection) / static_cast(coverHeight); + const float cropX = 1.0f - (tileRatio / ratio); renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, - tileWidth - 2 * hPaddingInSelection, Lyra3CoversMetrics::values.homeCoverHeight, - cropX); + tileWidth - 2 * hPaddingInSelection, coverHeight, cropX); } else { hasCover = false; } file.close(); + } else { + hasCover = false; } } // Draw either way renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, tileWidth - 2 * hPaddingInSelection, - Lyra3CoversMetrics::values.homeCoverHeight, true); + coverHeight, true); if (!hasCover) { // Render empty cover - renderer.fillRect(tileX + hPaddingInSelection, - tileY + hPaddingInSelection + (Lyra3CoversMetrics::values.homeCoverHeight / 3), - tileWidth - 2 * hPaddingInSelection, 2 * Lyra3CoversMetrics::values.homeCoverHeight / 3, - true); + renderer.fillRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection + (coverHeight / 3), + tileWidth - 2 * hPaddingInSelection, 2 * coverHeight / 3, true); renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32, 32); } } @@ -99,16 +99,14 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con // Draw selection box renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false, Color::LightGray); - renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection, - Lyra3CoversMetrics::values.homeCoverHeight, Color::LightGray); + renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection, coverHeight, Color::LightGray); renderer.fillRectDither(tileX + tileWidth - hPaddingInSelection, tileY + hPaddingInSelection, - hPaddingInSelection, Lyra3CoversMetrics::values.homeCoverHeight, Color::LightGray); - renderer.fillRoundedRect(tileX, tileY + Lyra3CoversMetrics::values.homeCoverHeight + hPaddingInSelection, - tileWidth, dynamicTitleBoxHeight, cornerRadius, false, false, true, true, - Color::LightGray); + hPaddingInSelection, coverHeight, Color::LightGray); + renderer.fillRoundedRect(tileX, tileY + coverHeight + hPaddingInSelection, tileWidth, dynamicTitleBoxHeight, + cornerRadius, false, false, true, true, Color::LightGray); } - int currentY = tileY + Lyra3CoversMetrics::values.homeCoverHeight + hPaddingInSelection + 5; + int currentY = tileY + coverHeight + hPaddingInSelection + 5; for (const auto& line : titleLines) { renderer.drawText(SMALL_FONT_ID, tileX + hPaddingInSelection, currentY, line.c_str(), true); currentY += titleLineHeight; diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index fcdf474e..b31c2611 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -43,6 +43,7 @@ constexpr int maxListValueWidth = 200; constexpr int mainMenuIconSize = 32; constexpr int listIconSize = 24; constexpr int mainMenuColumns = 2; +constexpr int minAdaptiveMenuRowHeight = 40; int coverWidth = 0; void drawLyraBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight, @@ -430,9 +431,10 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: const int tileWidth = rect.width - 2 * LyraMetrics::values.contentSidePadding; const int tileHeight = rect.height; const int tileY = rect.y; + const int coverHeight = std::max(120, tileHeight - 2 * hPaddingInSelection); const bool hasContinueReading = !recentBooks.empty(); if (coverWidth == 0) { - coverWidth = LyraMetrics::values.homeCoverHeight * 0.6; + coverWidth = static_cast(coverHeight * 0.6f); } // Draw book card regardless, fill with message based on `hasContinueReading` @@ -447,7 +449,7 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: if (coverPath.empty()) { hasCover = false; } else { - const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::values.homeCoverHeight); + const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, coverHeight); // First time: load cover from SD and render FsFile file; @@ -456,7 +458,7 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: if (bitmap.parseHeaders() == BmpReaderError::Ok) { coverWidth = bitmap.getWidth(); renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth, - LyraMetrics::values.homeCoverHeight); + coverHeight); } else { hasCover = false; } @@ -465,14 +467,12 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: } // Draw either way - renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth, - LyraMetrics::values.homeCoverHeight, true); + renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth, coverHeight, true); if (!hasCover) { // Render empty cover - renderer.fillRect(tileX + hPaddingInSelection, - tileY + hPaddingInSelection + (LyraMetrics::values.homeCoverHeight / 3), coverWidth, - 2 * LyraMetrics::values.homeCoverHeight / 3, true); + renderer.fillRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection + (coverHeight / 3), coverWidth, + 2 * coverHeight / 3, true); renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32, 32); } @@ -489,13 +489,11 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: // Draw selection box renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false, Color::LightGray); - renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection, - LyraMetrics::values.homeCoverHeight, Color::LightGray); + renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection, coverHeight, Color::LightGray); renderer.fillRectDither(tileX + hPaddingInSelection + coverWidth, tileY + hPaddingInSelection, - tileWidth - hPaddingInSelection - coverWidth, LyraMetrics::values.homeCoverHeight, - Color::LightGray); - renderer.fillRoundedRect(tileX, tileY + LyraMetrics::values.homeCoverHeight + hPaddingInSelection, tileWidth, - hPaddingInSelection, cornerRadius, false, false, true, true, Color::LightGray); + tileWidth - hPaddingInSelection - coverWidth, coverHeight, Color::LightGray); + renderer.fillRoundedRect(tileX, tileY + coverHeight + hPaddingInSelection, tileWidth, hPaddingInSelection, + cornerRadius, false, false, true, true, Color::LightGray); } auto titleLines = renderer.wrappedText(UI_12_FONT_ID, book.title.c_str(), textWidth, 3, EpdFontFamily::BOLD); @@ -537,11 +535,28 @@ void LyraTheme::drawEmptyRecents(const GfxRenderer& renderer, const Rect rect) c void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex, const std::function& buttonLabel, const std::function& rowIcon) const { + int rowHeight = LyraMetrics::values.menuRowHeight; + int rowSpacing = LyraMetrics::values.menuSpacing; + if (buttonCount > 0 && rect.height > 0) { + const int defaultHeight = buttonCount * rowHeight + std::max(0, buttonCount - 1) * rowSpacing; + if (defaultHeight > rect.height) { + const int spacingSlots = std::max(1, buttonCount - 1); + rowSpacing = std::max(0, (rect.height - buttonCount * rowHeight) / spacingSlots); + if (buttonCount * rowHeight + std::max(0, buttonCount - 1) * rowSpacing > rect.height) { + rowHeight = + std::max(minAdaptiveMenuRowHeight, (rect.height - std::max(0, buttonCount - 1) * rowSpacing) / buttonCount); + } + if (buttonCount * rowHeight + std::max(0, buttonCount - 1) * rowSpacing > rect.height) { + rowHeight = std::max(1, rect.height / buttonCount); + rowSpacing = 0; + } + } + } + for (int i = 0; i < buttonCount; ++i) { int tileWidth = rect.width - LyraMetrics::values.contentSidePadding * 2; - Rect tileRect = Rect{rect.x + LyraMetrics::values.contentSidePadding, - rect.y + i * (LyraMetrics::values.menuRowHeight + LyraMetrics::values.menuSpacing), tileWidth, - LyraMetrics::values.menuRowHeight}; + Rect tileRect = Rect{rect.x + LyraMetrics::values.contentSidePadding, rect.y + i * (rowHeight + rowSpacing), + tileWidth, rowHeight}; const bool selected = selectedIndex == i; @@ -553,7 +568,7 @@ void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount const char* label = labelStr.c_str(); int textX = tileRect.x + 16; const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); - const int textY = tileRect.y + (LyraMetrics::values.menuRowHeight - lineHeight) / 2; + const int textY = tileRect.y + (rowHeight - lineHeight) / 2; if (rowIcon != nullptr) { UIIcon icon = rowIcon(i); From 07f0e49a7dbe82ef7f4c195e1423f0e5fa66496c Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sun, 5 Apr 2026 22:50:22 +0200 Subject: [PATCH 2/2] Fix size mismatch preventing thumbnail display --- src/components/themes/lyra/Lyra3CoversTheme.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/themes/lyra/Lyra3CoversTheme.cpp b/src/components/themes/lyra/Lyra3CoversTheme.cpp index 5e568174..4d9f403e 100644 --- a/src/components/themes/lyra/Lyra3CoversTheme.cpp +++ b/src/components/themes/lyra/Lyra3CoversTheme.cpp @@ -16,6 +16,7 @@ namespace { constexpr int hPaddingInSelection = 8; constexpr int cornerRadius = 6; +constexpr int coverHeightOffset = 58; } // namespace void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector& recentBooks, @@ -23,8 +24,7 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con bool& bufferRestored, std::function storeCoverBuffer) const { const int tileWidth = (rect.width - 2 * Lyra3CoversMetrics::values.contentSidePadding) / 3; const int tileY = rect.y; - const int titleAreaHeight = renderer.getLineHeight(SMALL_FONT_ID) * 3 + hPaddingInSelection + 5; - const int coverHeight = std::max(120, rect.height - titleAreaHeight); + const int coverHeight = std::max(120, rect.height - coverHeightOffset); const bool hasContinueReading = !recentBooks.empty(); // Draw book card regardless, fill with message based on `hasContinueReading`