From 1e20d30875df3967910d11258d4f186f029ea75f Mon Sep 17 00:00:00 2001 From: cottongin Date: Sat, 24 Jan 2026 02:44:11 -0500 Subject: [PATCH] yep it works --- src/activities/home/HomeActivity.cpp | 390 ++++++++++++++------------- 1 file changed, 199 insertions(+), 191 deletions(-) diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index eb11ba9..0f0e8ed 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -218,12 +219,32 @@ void HomeActivity::render() { constexpr int margin = 20; constexpr int bottomMargin = 60; - - // --- Top "book" card for the current title (selectorIndex == 0) --- - const int bookWidth = pageWidth / 2; - const int bookHeight = pageHeight / 2; - const int bookX = (pageWidth - bookWidth) / 2; constexpr int bookY = 30; + constexpr int elementSpacing = 15; + + // --- Calculate layout from bottom up --- + + // Build menu items dynamically (need count for layout calculation) + std::vector menuItems = {"My Library", "File Transfer", "Settings"}; + if (hasOpdsUrl) { + menuItems.insert(menuItems.begin() + 1, "Calibre Library"); + } + + const int menuTileWidth = pageWidth - 2 * margin; + constexpr int menuTileHeight = 45; + constexpr int menuSpacing = 8; + const int totalMenuHeight = + static_cast(menuItems.size()) * menuTileHeight + (static_cast(menuItems.size()) - 1) * menuSpacing; + + // Anchor menu to bottom of screen + const int menuStartY = pageHeight - bottomMargin - totalMenuHeight - margin; + + // Calculate book card dimensions - larger, filling available space + const int bookWidth = pageWidth - 2 * margin; + // Card extends to just above menu + const int bookCardBottomY = menuStartY - elementSpacing; + const int bookHeight = bookCardBottomY - bookY; + const int bookX = margin; const bool bookSelected = hasContinueReading && selectorIndex == 0; // Bookmark dimensions (used in multiple places) @@ -242,27 +263,26 @@ void HomeActivity::render() { if (SdMan.openFileForRead("HOME", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { - // Calculate position to center image within the book card - int coverX, coverY; + // Add padding around the cover image so it doesn't touch the frame + constexpr int coverPadding = 10; + const int availableWidth = bookWidth - 2 * coverPadding; + const int availableHeight = bookHeight - 2 * coverPadding; - if (bitmap.getWidth() > bookWidth || bitmap.getHeight() > bookHeight) { - const float imgRatio = static_cast(bitmap.getWidth()) / static_cast(bitmap.getHeight()); - const float boxRatio = static_cast(bookWidth) / static_cast(bookHeight); + // Calculate scale to fit image within padded area while maintaining aspect ratio + const float scaleX = static_cast(availableWidth) / static_cast(bitmap.getWidth()); + const float scaleY = static_cast(availableHeight) / static_cast(bitmap.getHeight()); + const float scale = std::min(scaleX, scaleY); - if (imgRatio > boxRatio) { - coverX = bookX; - coverY = bookY + (bookHeight - static_cast(bookWidth / imgRatio)) / 2; - } else { - coverX = bookX + (bookWidth - static_cast(bookHeight * imgRatio)) / 2; - coverY = bookY; - } - } else { - coverX = bookX + (bookWidth - bitmap.getWidth()) / 2; - coverY = bookY + (bookHeight - bitmap.getHeight()) / 2; - } + // Calculate actual scaled dimensions + const int scaledWidth = static_cast(bitmap.getWidth() * scale); + const int scaledHeight = static_cast(bitmap.getHeight() * scale); + + // Center the scaled image within the book card (accounting for padding) + const int coverX = bookX + (bookWidth - scaledWidth) / 2; + const int coverY = bookY + (bookHeight - scaledHeight) / 2; // Draw the cover image centered within the book card - renderer.drawBitmap(bitmap, coverX, coverY, bookWidth, bookHeight); + renderer.drawBitmap(bitmap, coverX, coverY, scaledWidth, scaledHeight); // Draw border around the card renderer.drawRect(bookX, bookY, bookWidth, bookHeight); @@ -325,169 +345,177 @@ void HomeActivity::render() { } if (hasContinueReading) { - // Invert text colors based on selection state: - // - With cover: selected = white text on black box, unselected = black text on white box - // - Without cover: selected = white text on black card, unselected = black text on white card - - // Split into words (avoid stringstream to keep this light on the MCU) - std::vector words; - words.reserve(8); - size_t pos = 0; - while (pos < lastBookTitle.size()) { - while (pos < lastBookTitle.size() && lastBookTitle[pos] == ' ') { - ++pos; - } - if (pos >= lastBookTitle.size()) { - break; - } - const size_t start = pos; - while (pos < lastBookTitle.size() && lastBookTitle[pos] != ' ') { - ++pos; - } - words.emplace_back(lastBookTitle.substr(start, pos - start)); - } - - std::vector lines; - std::string currentLine; - // Extra padding inside the card so text doesn't hug the border - const int maxLineWidth = bookWidth - 40; - const int spaceWidth = renderer.getSpaceWidth(UI_12_FONT_ID); - - for (auto& i : words) { - // If we just hit the line limit (3), stop processing words - if (lines.size() >= 3) { - // Limit to 3 lines - // Still have words left, so add ellipsis to last line - lines.back().append("..."); - - while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) { - // Remove "..." first, then remove one UTF-8 char, then add "..." back - lines.back().resize(lines.back().size() - 3); // Remove "..." - StringUtils::utf8RemoveLastChar(lines.back()); - lines.back().append("..."); - } - break; - } - - int wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str()); - while (wordWidth > maxLineWidth && !i.empty()) { - // Word itself is too long, trim it (UTF-8 safe) - StringUtils::utf8RemoveLastChar(i); - // Check if we have room for ellipsis - std::string withEllipsis = i + "..."; - wordWidth = renderer.getTextWidth(UI_12_FONT_ID, withEllipsis.c_str()); - if (wordWidth <= maxLineWidth) { - i = withEllipsis; - break; - } - } - - int newLineWidth = renderer.getTextWidth(UI_12_FONT_ID, currentLine.c_str()); - if (newLineWidth > 0) { - newLineWidth += spaceWidth; - } - newLineWidth += wordWidth; - - if (newLineWidth > maxLineWidth && !currentLine.empty()) { - // New line too long, push old line - lines.push_back(currentLine); - currentLine = i; - } else { - currentLine.append(" ").append(i); - } - } - - // If lower than the line limit, push remaining words - if (!currentLine.empty() && lines.size() < 3) { - lines.push_back(currentLine); - } - - // Book title text - int totalTextHeight = renderer.getLineHeight(UI_12_FONT_ID) * static_cast(lines.size()); - if (!lastBookAuthor.empty()) { - totalTextHeight += renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2; - } - - // Vertically center the title block within the card - int titleYStart = bookY + (bookHeight - totalTextHeight) / 2; - - // If cover image was rendered, draw box behind title and author if (coverRendered) { + // --- Cover image present: draw combined label at the bottom of the card --- + // Box contains: "Continue Reading" (larger) and "Title - Author" (smaller) + + const char* continueText = "Continue Reading"; constexpr int boxPadding = 8; - // Calculate the max text width for the box - int maxTextWidth = 0; - for (const auto& line : lines) { - const int lineWidth = renderer.getTextWidth(UI_12_FONT_ID, line.c_str()); - if (lineWidth > maxTextWidth) { - maxTextWidth = lineWidth; - } - } + constexpr int lineSpacing = 2; + + // Build subtitle: "Title - Author" or just "Title" + std::string subtitle = lastBookTitle; if (!lastBookAuthor.empty()) { - std::string trimmedAuthor = lastBookAuthor; - while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) { - StringUtils::utf8RemoveLastChar(trimmedAuthor); - } - if (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) < - renderer.getTextWidth(UI_10_FONT_ID, lastBookAuthor.c_str())) { - trimmedAuthor.append("..."); - } - const int authorWidth = renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()); - if (authorWidth > maxTextWidth) { - maxTextWidth = authorWidth; - } + subtitle += " - " + lastBookAuthor; } - const int boxWidth = maxTextWidth + boxPadding * 2; - const int boxHeight = totalTextHeight + boxPadding * 2; - const int boxX = (pageWidth - boxWidth) / 2; - const int boxY = titleYStart - boxPadding; + // Calculate box dimensions based on both lines + const int continueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, continueText); + const int continueLineHeight = renderer.getLineHeight(UI_10_FONT_ID); + const int subtitleLineHeight = renderer.getLineHeight(SMALL_FONT_ID); - // Draw box (inverted when selected: black box instead of white) - renderer.fillRect(boxX, boxY, boxWidth, boxHeight, bookSelected); - // Draw border around the box (inverted when selected: white border instead of black) - renderer.drawRect(boxX, boxY, boxWidth, boxHeight, !bookSelected); - } - - for (const auto& line : lines) { - renderer.drawCenteredText(UI_12_FONT_ID, titleYStart, line.c_str(), !bookSelected); - titleYStart += renderer.getLineHeight(UI_12_FONT_ID); - } - - if (!lastBookAuthor.empty()) { - titleYStart += renderer.getLineHeight(UI_10_FONT_ID) / 2; - std::string trimmedAuthor = lastBookAuthor; - // Trim author if too long (UTF-8 safe) + // Truncate subtitle to fit within card (with padding) + const int maxSubtitleWidth = bookWidth - 2 * boxPadding - 20; // Extra margin for aesthetics bool wasTrimmed = false; - while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) { - StringUtils::utf8RemoveLastChar(trimmedAuthor); + while (renderer.getTextWidth(SMALL_FONT_ID, subtitle.c_str()) > maxSubtitleWidth && !subtitle.empty()) { + StringUtils::utf8RemoveLastChar(subtitle); wasTrimmed = true; } - if (wasTrimmed && !trimmedAuthor.empty()) { + if (wasTrimmed && !subtitle.empty()) { // Make room for ellipsis - while (renderer.getTextWidth(UI_10_FONT_ID, (trimmedAuthor + "...").c_str()) > maxLineWidth && - !trimmedAuthor.empty()) { - StringUtils::utf8RemoveLastChar(trimmedAuthor); + while (renderer.getTextWidth(SMALL_FONT_ID, (subtitle + "...").c_str()) > maxSubtitleWidth && + !subtitle.empty()) { + StringUtils::utf8RemoveLastChar(subtitle); } - trimmedAuthor.append("..."); + subtitle.append("..."); } - renderer.drawCenteredText(UI_10_FONT_ID, titleYStart, trimmedAuthor.c_str(), !bookSelected); - } - // "Continue Reading" label at the bottom - const int continueY = bookY + bookHeight - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2; - if (coverRendered) { - // Draw box behind "Continue Reading" text (inverted when selected: black box instead of white) - const char* continueText = "Continue Reading"; - const int continueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, continueText); - constexpr int continuePadding = 6; - const int continueBoxWidth = continueTextWidth + continuePadding * 2; - const int continueBoxHeight = renderer.getLineHeight(UI_10_FONT_ID) + continuePadding; - const int continueBoxX = (pageWidth - continueBoxWidth) / 2; - const int continueBoxY = continueY - continuePadding / 2; - renderer.fillRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, bookSelected); - renderer.drawRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, !bookSelected); + const int subtitleTextWidth = renderer.getTextWidth(SMALL_FONT_ID, subtitle.c_str()); + + // Box width is the wider of the two lines plus padding + const int boxContentWidth = std::max(continueTextWidth, subtitleTextWidth); + const int boxWidth = boxContentWidth + boxPadding * 2; + const int boxHeight = continueLineHeight + lineSpacing + subtitleLineHeight + boxPadding * 2; + + // Position box at the bottom of the card, centered + const int boxX = (pageWidth - boxWidth) / 2; + const int boxY = bookY + bookHeight - boxHeight - boxPadding; + + // Draw box background and border + renderer.fillRect(boxX, boxY, boxWidth, boxHeight, bookSelected); + renderer.drawRect(boxX, boxY, boxWidth, boxHeight, !bookSelected); + + // Draw "Continue Reading" line + const int continueY = boxY + boxPadding; renderer.drawCenteredText(UI_10_FONT_ID, continueY, continueText, !bookSelected); + + // Draw "Title - Author" line below + const int subtitleY = continueY + continueLineHeight + lineSpacing; + renderer.drawCenteredText(SMALL_FONT_ID, subtitleY, subtitle.c_str(), !bookSelected); } else { + // --- No cover image: draw title/author inside the card (existing behavior) --- + // Invert text colors based on selection state + + // Split into words (avoid stringstream to keep this light on the MCU) + std::vector words; + words.reserve(8); + size_t pos = 0; + while (pos < lastBookTitle.size()) { + while (pos < lastBookTitle.size() && lastBookTitle[pos] == ' ') { + ++pos; + } + if (pos >= lastBookTitle.size()) { + break; + } + const size_t start = pos; + while (pos < lastBookTitle.size() && lastBookTitle[pos] != ' ') { + ++pos; + } + words.emplace_back(lastBookTitle.substr(start, pos - start)); + } + + std::vector lines; + std::string currentLine; + // Extra padding inside the card so text doesn't hug the border + const int maxLineWidth = bookWidth - 40; + const int spaceWidth = renderer.getSpaceWidth(UI_12_FONT_ID); + + for (auto& i : words) { + // If we just hit the line limit (3), stop processing words + if (lines.size() >= 3) { + // Limit to 3 lines + // Still have words left, so add ellipsis to last line + lines.back().append("..."); + + while (!lines.back().empty() && + renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) { + // Remove "..." first, then remove one UTF-8 char, then add "..." back + lines.back().resize(lines.back().size() - 3); // Remove "..." + StringUtils::utf8RemoveLastChar(lines.back()); + lines.back().append("..."); + } + break; + } + + int wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str()); + while (wordWidth > maxLineWidth && !i.empty()) { + // Word itself is too long, trim it (UTF-8 safe) + StringUtils::utf8RemoveLastChar(i); + // Check if we have room for ellipsis + std::string withEllipsis = i + "..."; + wordWidth = renderer.getTextWidth(UI_12_FONT_ID, withEllipsis.c_str()); + if (wordWidth <= maxLineWidth) { + i = withEllipsis; + break; + } + } + + int newLineWidth = renderer.getTextWidth(UI_12_FONT_ID, currentLine.c_str()); + if (newLineWidth > 0) { + newLineWidth += spaceWidth; + } + newLineWidth += wordWidth; + + if (newLineWidth > maxLineWidth && !currentLine.empty()) { + // New line too long, push old line + lines.push_back(currentLine); + currentLine = i; + } else { + currentLine.append(" ").append(i); + } + } + + // If lower than the line limit, push remaining words + if (!currentLine.empty() && lines.size() < 3) { + lines.push_back(currentLine); + } + + // Book title text + int totalTextHeight = renderer.getLineHeight(UI_12_FONT_ID) * static_cast(lines.size()); + if (!lastBookAuthor.empty()) { + totalTextHeight += renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2; + } + + // Vertically center the title block within the card + int titleYStart = bookY + (bookHeight - totalTextHeight) / 2; + + for (const auto& line : lines) { + renderer.drawCenteredText(UI_12_FONT_ID, titleYStart, line.c_str(), !bookSelected); + titleYStart += renderer.getLineHeight(UI_12_FONT_ID); + } + + if (!lastBookAuthor.empty()) { + titleYStart += renderer.getLineHeight(UI_10_FONT_ID) / 2; + std::string trimmedAuthor = lastBookAuthor; + // Trim author if too long (UTF-8 safe) + bool wasTrimmed = false; + while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) { + StringUtils::utf8RemoveLastChar(trimmedAuthor); + wasTrimmed = true; + } + if (wasTrimmed && !trimmedAuthor.empty()) { + // Make room for ellipsis + while (renderer.getTextWidth(UI_10_FONT_ID, (trimmedAuthor + "...").c_str()) > maxLineWidth && + !trimmedAuthor.empty()) { + StringUtils::utf8RemoveLastChar(trimmedAuthor); + } + trimmedAuthor.append("..."); + } + renderer.drawCenteredText(UI_10_FONT_ID, titleYStart, trimmedAuthor.c_str(), !bookSelected); + } + + // "Continue Reading" label at the bottom of card (only when no cover) + const int continueY = bookY + bookHeight - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2; renderer.drawCenteredText(UI_10_FONT_ID, continueY, "Continue Reading", !bookSelected); } } else { @@ -498,27 +526,7 @@ void HomeActivity::render() { renderer.drawCenteredText(UI_10_FONT_ID, y + renderer.getLineHeight(UI_12_FONT_ID), "Start reading below"); } - // --- Bottom menu tiles --- - // Build menu items dynamically - std::vector menuItems = {"My Library", "File Transfer", "Settings"}; - if (hasOpdsUrl) { - // Insert Calibre Library after My Library - menuItems.insert(menuItems.begin() + 1, "Calibre Library"); - } - - const int menuTileWidth = pageWidth - 2 * margin; - constexpr int menuTileHeight = 45; - constexpr int menuSpacing = 8; - const int totalMenuHeight = - static_cast(menuItems.size()) * menuTileHeight + (static_cast(menuItems.size()) - 1) * menuSpacing; - - int menuStartY = bookY + bookHeight + 15; - // Ensure we don't collide with the bottom button legend - const int maxMenuStartY = pageHeight - bottomMargin - totalMenuHeight - margin; - if (menuStartY > maxMenuStartY) { - menuStartY = maxMenuStartY; - } - + // --- Bottom menu tiles (anchored to bottom) --- for (size_t i = 0; i < menuItems.size(); ++i) { const int overallIndex = static_cast(i) + (hasContinueReading ? 1 : 0); constexpr int tileX = margin;