diff --git a/chat-summaries/2026-03-09_04-30-summary.md b/chat-summaries/2026-03-09_04-30-summary.md new file mode 100644 index 00000000..7c9504b1 --- /dev/null +++ b/chat-summaries/2026-03-09_04-30-summary.md @@ -0,0 +1,26 @@ +# BookInfo: Landscape Side-by-Side Layout + +**Date**: 2026-03-09 +**Task**: Implement a side-by-side layout for BookInfo in landscape orientation -- cover fixed on the left, scrollable metadata fields on the right. Portrait layout unchanged. + +## Changes Made + +### `src/activities/home/BookInfoActivity.h` + +- Added `bool isLandscape` and `int coverPanelWidth` member variables. + +### `src/activities/home/BookInfoActivity.cpp` + +- **`onEnter()`**: Detects orientation via `renderer.getOrientation()`. In landscape, the cover thumbnail height fills the full content area (between header and button hints) instead of 2/5 of screen height. +- **`buildLayout()`**: Reads cover bitmap dimensions first. In landscape with a cover, computes `coverPanelWidth` (cover width + padding, capped at 40% of screen). Text fields are wrapped to the narrower right-panel width. Cover height is excluded from `contentHeight` (it sits beside, not above, the fields). +- **`render()`**: In landscape, draws the cover at a fixed position in the left panel (unaffected by scroll). Fields render in the right panel at `x = coverPanelWidth`. In portrait, existing behavior is preserved (cover above fields, both scroll together). The header fill + draw-on-top pattern continues to prevent content bleeding into the header zone. + +## Behavior Summary + +- **Portrait** (480x800): Cover on top, fields below, everything scrolls vertically (no change). +- **Landscape** (800x480): Cover pinned on left (centered vertically, fills content height), metadata fields scroll independently on the right. +- If no cover exists in landscape, the full screen width is used for fields (same as portrait but in landscape dimensions). + +## Follow-up + +None -- ready for hardware testing in all 4 orientations. diff --git a/src/activities/home/BookInfoActivity.cpp b/src/activities/home/BookInfoActivity.cpp index eefe46c6..ec243cc3 100644 --- a/src/activities/home/BookInfoActivity.cpp +++ b/src/activities/home/BookInfoActivity.cpp @@ -43,6 +43,10 @@ std::string normalizeWhitespace(const std::string& s) { void BookInfoActivity::onEnter() { Activity::onEnter(); + const auto orient = renderer.getOrientation(); + isLandscape = orient == GfxRenderer::Orientation::LandscapeClockwise || + orient == GfxRenderer::Orientation::LandscapeCounterClockwise; + std::string fileName = filePath; const size_t lastSlash = filePath.rfind('/'); if (lastSlash != std::string::npos) { @@ -58,6 +62,11 @@ void BookInfoActivity::onEnter() { } } + const auto& metrics = UITheme::getInstance().getMetrics(); + const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; + const int contentBottom = renderer.getScreenHeight() - metrics.buttonHintsHeight - metrics.verticalSpacing; + const int coverH = isLandscape ? (contentBottom - contentTop) : (renderer.getScreenHeight() * 2 / 5); + BookMetadataCache::BookMetadata meta; if (FsHelpers::hasEpubExtension(fileName)) { @@ -74,7 +83,6 @@ void BookInfoActivity::onEnter() { meta = epub.getMetadata(); meta.description = normalizeWhitespace(meta.description); - const int coverH = renderer.getScreenHeight() * 2 / 5; if (epub.generateThumbBmp(coverH)) { coverBmpPath = epub.getThumbBmpPath(coverH); } else { @@ -103,7 +111,6 @@ void BookInfoActivity::onEnter() { meta.title = xtc.getTitle(); meta.author = xtc.getAuthor(); - const int coverH = renderer.getScreenHeight() * 2 / 5; if (xtc.generateThumbBmp(coverH)) { coverBmpPath = xtc.getThumbBmpPath(coverH); } else { @@ -132,8 +139,30 @@ void BookInfoActivity::onExit() { Activity::onExit(); } void BookInfoActivity::buildLayout(const BookMetadataCache::BookMetadata& meta, size_t fileSize) { const auto& metrics = UITheme::getInstance().getMetrics(); + const int pageW = renderer.getScreenWidth(); const int sidePad = metrics.contentSidePadding; - const int contentW = renderer.getScreenWidth() - sidePad * 2; + + if (!coverBmpPath.empty()) { + FsFile file; + if (Storage.openFileForRead("BIF", coverBmpPath, file)) { + Bitmap bitmap(file); + if (bitmap.parseHeaders() == BmpReaderError::Ok) { + coverDisplayHeight = bitmap.getHeight(); + coverDisplayWidth = bitmap.getWidth(); + } + file.close(); + } + } + + coverPanelWidth = 0; + if (isLandscape && coverDisplayWidth > 0) { + coverPanelWidth = std::min(coverDisplayWidth + sidePad * 2, pageW * 2 / 5); + } + + const int contentW = isLandscape && coverPanelWidth > 0 + ? pageW - coverPanelWidth - sidePad + : pageW - sidePad * 2; + fields.reserve(13); auto addField = [&](const char* label, const std::string& text, bool bold, EpdFontFamily::Style style) { @@ -185,19 +214,8 @@ void BookInfoActivity::buildLayout(const BookMetadataCache::BookMetadata& meta, const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; int h = contentTop; - if (!coverBmpPath.empty()) { - FsFile file; - if (Storage.openFileForRead("BIF", coverBmpPath, file)) { - Bitmap bitmap(file); - if (bitmap.parseHeaders() == BmpReaderError::Ok) { - coverDisplayHeight = bitmap.getHeight(); - coverDisplayWidth = bitmap.getWidth(); - } - file.close(); - } - if (coverDisplayHeight > 0) { - h += coverDisplayHeight + COVER_GAP; - } + if (!isLandscape && coverDisplayHeight > 0 && !coverBmpPath.empty()) { + h += coverDisplayHeight + COVER_GAP; } for (const auto& field : fields) { @@ -255,9 +273,23 @@ void BookInfoActivity::render(RenderLock&&) { const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; const int contentBottom = pageH - metrics.buttonHintsHeight - metrics.verticalSpacing; + if (isLandscape && coverPanelWidth > 0 && !coverBmpPath.empty() && coverDisplayHeight > 0) { + FsFile file; + if (Storage.openFileForRead("BIF", coverBmpPath, file)) { + Bitmap bitmap(file); + if (bitmap.parseHeaders() == BmpReaderError::Ok) { + const int availH = contentBottom - contentTop; + const int coverX = (coverPanelWidth - coverDisplayWidth) / 2; + renderer.drawBitmap1Bit(bitmap, coverX, contentTop, coverDisplayWidth, availH); + } + file.close(); + } + } + + const int fieldX = (isLandscape && coverPanelWidth > 0) ? coverPanelWidth : sidePad; int y = contentTop - scrollOffset; - if (!coverBmpPath.empty() && coverDisplayHeight > 0) { + if (!isLandscape && !coverBmpPath.empty() && coverDisplayHeight > 0) { if (y + coverDisplayHeight > 0 && y < contentBottom) { FsFile file; if (Storage.openFileForRead("BIF", coverBmpPath, file)) { @@ -277,7 +309,7 @@ void BookInfoActivity::render(RenderLock&&) { if (field.label) { if (y + lineH10 > contentTop && y < contentBottom) { - renderer.drawText(UI_10_FONT_ID, sidePad, y, field.label, true, EpdFontFamily::BOLD); + renderer.drawText(UI_10_FONT_ID, fieldX, y, field.label, true, EpdFontFamily::BOLD); } y += lineH10 + LABEL_VALUE_GAP; } @@ -286,7 +318,7 @@ void BookInfoActivity::render(RenderLock&&) { for (const auto& line : field.lines) { if (y >= contentBottom) break; if (y + lineH12 > contentTop) { - renderer.drawText(UI_12_FONT_ID, sidePad, y, line.c_str(), true, style); + renderer.drawText(UI_12_FONT_ID, fieldX, y, line.c_str(), true, style); } y += lineH12; } diff --git a/src/activities/home/BookInfoActivity.h b/src/activities/home/BookInfoActivity.h index 50cc4ad6..530ab424 100644 --- a/src/activities/home/BookInfoActivity.h +++ b/src/activities/home/BookInfoActivity.h @@ -32,6 +32,8 @@ class BookInfoActivity final : public Activity { int contentHeight = 0; int coverDisplayHeight = 0; int coverDisplayWidth = 0; + int coverPanelWidth = 0; + bool isLandscape = false; void buildLayout(const BookMetadataCache::BookMetadata& meta, size_t fileSize); static std::string formatFileSize(size_t bytes);