Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
13 kB · 261 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262#include "RoundedRaffTheme.h"
#include <GfxRenderer.h>#include <HalGPIO.h>#include <HalStorage.h>#include <I18n.h>
#include <algorithm>#include <string>#include <vector>
#include "RecentBooksStore.h"#include "components/UITheme.h"#include "components/icons/cover.h"#include "fontIds.h"
namespace {constexpr int kCoverRadius = 18;constexpr int kMenuRadius = 30;constexpr int kBottomRadius = 15;constexpr int kRowRadius = 20;constexpr int kInteractiveInsetX = 20;constexpr int kSelectableRowGap = 6;constexpr int kTitleFontId = UI_12_FONT_ID; // Requested main title size: 12pxconstexpr int kGuideFontId = SMALL_FONT_ID; // Closest available to requested 6px
void drawScrollBar(const GfxRenderer& renderer, Rect rect, int itemCount, int pageStartIndex, int pageItems) { if (itemCount <= 0 || pageItems <= 0 || itemCount <= pageItems) { return; }
const int barW = RoundedRaffMetrics::values.scrollBarWidth; const int barX = rect.x + rect.width - RoundedRaffMetrics::values.scrollBarRightOffset - barW; const int barY = rect.y; const int barH = rect.height;
const int thumbH = std::max(10, (barH * pageItems) / itemCount); const int maxStart = std::max(1, itemCount - pageItems); const int maxTravel = std::max(1, barH - thumbH); const int clampedStart = std::clamp(pageStartIndex, 0, maxStart); const int thumbY = barY + (clampedStart * maxTravel) / maxStart;
renderer.fillRect(barX, thumbY, barW, thumbH);}
} // namespaceint coverWidth = 0;
void RoundedRaffTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle, const bool backButton) const { // Home screen header is custom-rendered in drawRecentBookCover. if (title == nullptr) { return; } BaseTheme::drawHeader(renderer, rect, title, subtitle, backButton);}
void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks, const int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored, std::function<bool()> storeCoverBuffer) const { const int tileWidth = rect.width - 2 * RoundedRaffMetrics::values.contentSidePadding; const int tileHeight = rect.height; const int tileY = rect.y; const bool hasContinueReading = !recentBooks.empty(); if (coverWidth == 0) { coverWidth = RoundedRaffMetrics::values.homeCoverHeight * 0.6; } const int imgY = tileY + (tileHeight - RoundedRaffMetrics::values.homeCoverHeight) / 2; const int tileX = RoundedRaffMetrics::values.contentSidePadding;
// Draw book card regardless, fill with message based on `hasContinueReading` // Draw cover image as background if available (inside the box) // Only load from SD on first render, then use stored buffer if (hasContinueReading) { RecentBook book = recentBooks[0]; if (!coverRendered) { std::string coverPath = book.coverBmpPath; bool hasCover = true; if (coverPath.empty()) { hasCover = false; } else { const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, RoundedRaffMetrics::values.homeCoverHeight);
// First time: load cover from SD and render HalFile file; if (Storage.openFileForRead("HOME", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { coverWidth = bitmap.getWidth(); // Narrow covers come out taller than the slot; fill 1:1 and crop // vertically instead of rescaling the dither. drawCoverThumbFill(renderer, bitmap, Rect{tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth, RoundedRaffMetrics::values.homeCoverHeight}); renderer.maskRoundedRectOutsideCorners(tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth, RoundedRaffMetrics::values.homeCoverHeight, kCoverRadius, Color::LightGray); } else { hasCover = false; } file.close(); } }
// Draw either way renderer.drawRoundedRect(tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth, RoundedRaffMetrics::values.homeCoverHeight, 1, kCoverRadius, true);
if (!hasCover) { // Render empty cover renderer.fillRect(tileX + (tileWidth - coverWidth) / 2, imgY + (RoundedRaffMetrics::values.homeCoverHeight / 3), coverWidth, 2 * RoundedRaffMetrics::values.homeCoverHeight / 3, true); renderer.drawIcon(CoverIcon, tileX + (tileWidth - coverWidth) / 2 + 24, imgY + 24, 32); renderer.maskRoundedRectOutsideCorners(tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth, RoundedRaffMetrics::values.homeCoverHeight, kCoverRadius, Color::LightGray); }
coverBufferStored = storeCoverBuffer(); coverRendered = coverBufferStored; // Only consider it rendered if we successfully stored the buffer }
renderer.fillRoundedRect(tileX, tileY, tileWidth, imgY - tileY, kRowRadius, true, true, false, false, Color::LightGray); renderer.fillRectDither(tileX, imgY, (tileWidth - coverWidth) / 2, RoundedRaffMetrics::values.homeCoverHeight, Color::LightGray); renderer.fillRectDither(tileX + (tileWidth + coverWidth) / 2, imgY, (tileWidth - coverWidth) / 2, RoundedRaffMetrics::values.homeCoverHeight, Color::LightGray); renderer.fillRoundedRect(tileX, imgY + RoundedRaffMetrics::values.homeCoverHeight, tileWidth, tileHeight - (imgY - tileY + RoundedRaffMetrics::values.homeCoverHeight), kRowRadius, false, false, true, true, Color::LightGray); } else { renderer.fillRoundedRect(tileX, tileY, tileWidth, tileHeight, kRowRadius, Color::LightGray); renderer.drawCenteredText(kTitleFontId, rect.y + rect.height / 2 - renderer.getLineHeight(kTitleFontId) / 2, tr(STR_NO_OPEN_BOOK)); }}
int RoundedRaffTheme::getMenuRowHeight(const GfxRenderer& renderer) const { return renderer.getLineHeight(kTitleFontId) + 20; // 10px top + 10px bottom}
void RoundedRaffTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex, const std::function<std::string(int index)>& buttonLabel, const std::function<UIIcon(int index)>& rowIcon) const { (void)rowIcon; const int sidePadding = RoundedRaffMetrics::values.contentSidePadding; const int rowX = rect.x + sidePadding; const int rowHeight = getMenuRowHeight(renderer); // shared with HomeActivity's touch grid const int rowGap = kSelectableRowGap; const int rowStep = rowHeight + rowGap; const int pageItems = std::max(1, rect.height / rowStep); const int safeSelectedIndex = std::max(0, selectedIndex); const int pageStartIndex = (safeSelectedIndex / pageItems) * pageItems; const int menuTop = rect.y; const int textLineHeight = renderer.getLineHeight(kTitleFontId); const int menuMaxWidth = std::max(0, rect.width - sidePadding * 2);
for (int i = pageStartIndex; i < buttonCount && i < pageStartIndex + pageItems; ++i) { const std::string label = buttonLabel(i); const int rowY = menuTop + (i - pageStartIndex) * rowStep; constexpr int kRowPaddingX = 40; // 20px L/R const int maxLabelWidth = std::max(0, menuMaxWidth - kRowPaddingX); const std::string truncatedLabel = renderer.truncatedText(kTitleFontId, label.c_str(), maxLabelWidth, EpdFontFamily::BOLD); const int rowWidth = std::min( menuMaxWidth, renderer.getTextWidth(kTitleFontId, truncatedLabel.c_str(), EpdFontFamily::BOLD) + kRowPaddingX); const bool isSelected = selectedIndex == i; renderer.fillRoundedRect(rowX, rowY, rowWidth, rowHeight, kMenuRadius, isSelected ? Color::Black : Color::White); const int textY = rowY + (rowHeight - textLineHeight) / 2; const int textX = rowX + kInteractiveInsetX; if (selectedIndex == i) { renderer.drawText(kTitleFontId, textX, textY, truncatedLabel.c_str(), false, EpdFontFamily::BOLD); } else { renderer.drawText(kTitleFontId, textX, textY, truncatedLabel.c_str(), true, EpdFontFamily::BOLD); } }
drawScrollBar(renderer, rect, buttonCount, pageStartIndex, pageItems);}
void RoundedRaffTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode, int contentStartX, int contentWidth) const { const auto& metrics = UITheme::getInstance().getMetrics(); const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); const int lineY = rect.y + rect.height + lineHeight + metrics.verticalSpacing; const int thickness = cursorMode ? 3 : 2;
if (contentWidth > 0) { renderer.drawLine(rect.x + contentStartX, lineY, rect.x + contentStartX + contentWidth - 1, lineY, thickness, true); return; }
constexpr int hPadding = 8; const int lineW = textWidth + hPadding * 2; const int lineStart = rect.x + (rect.width - lineW) / 2; renderer.drawLine(lineStart, lineY, lineStart + lineW - 1, lineY, thickness, true);}
void RoundedRaffTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3, const char* btn4) const { if (gpio.hasTouch()) { return; }
const GfxRenderer::Orientation origOrientation = renderer.getOrientation(); renderer.setOrientation(GfxRenderer::Orientation::Portrait);
const int pageWidth = renderer.getScreenWidth(); const int pageHeight = renderer.getScreenHeight(); const int sidePadding = 20; const int groupGap = 10; const int bottomMargin = 10; const int hintHeight = RoundedRaffMetrics::values.buttonHintsHeight - 10; // 30px total guide height const int groupWidth = (pageWidth - sidePadding * 2 - groupGap) / 2; const int hintY = pageHeight - hintHeight - bottomMargin; const int textY = hintY + (hintHeight - renderer.getLineHeight(kGuideFontId)) / 2;
if (renderer.getRenderMode() != GfxRenderer::BW && !renderer.grayPlanesAreAbsolute()) { renderer.fillRect(sidePadding, hintY, groupWidth, hintHeight, true); renderer.fillRect(sidePadding + groupWidth + groupGap, hintY, groupWidth, hintHeight, true); renderer.setOrientation(origOrientation); return; }
const bool backDisabled = (btn1 == nullptr || btn1[0] == '\0'); const int leftGroupX = sidePadding; const int rightGroupX = leftGroupX + groupWidth + groupGap; const std::string backLabel = backDisabled ? "" : std::string(btn1); // Callers should provide the button labels. If a label is not specified, it should render empty. const std::string selectText = (btn2 && btn2[0] != '\0') ? std::string(btn2) : ""; const std::string upText = (btn3 && btn3[0] != '\0') ? std::string(btn3) : ""; const std::string downText = (btn4 && btn4[0] != '\0') ? std::string(btn4) : "";
// Ensure button hints always "win" visually even if other elements accidentally render into this area. renderer.fillRect(leftGroupX, hintY, groupWidth, hintHeight, false); renderer.fillRect(rightGroupX, hintY, groupWidth, hintHeight, false);
renderer.drawRoundedRect(leftGroupX, hintY, groupWidth, hintHeight, 2, kBottomRadius, true); const int selectWidth = renderer.getTextWidth(kGuideFontId, selectText.c_str(), EpdFontFamily::REGULAR); const int downWidth = renderer.getTextWidth(kGuideFontId, downText.c_str(), EpdFontFamily::REGULAR); constexpr int innerEdgePadding = 16;
const int backX = leftGroupX + innerEdgePadding; const int selectX = leftGroupX + groupWidth - innerEdgePadding - selectWidth; const int upX = rightGroupX + innerEdgePadding; const int downX = rightGroupX + groupWidth - innerEdgePadding - downWidth;
if (!backDisabled) { renderer.drawText(kGuideFontId, backX, textY, backLabel.c_str(), true, EpdFontFamily::REGULAR); } renderer.drawText(kGuideFontId, selectX, textY, selectText.c_str(), true, EpdFontFamily::REGULAR);
renderer.drawRoundedRect(rightGroupX, hintY, groupWidth, hintHeight, 2, kBottomRadius, true);
renderer.drawText(kGuideFontId, upX, textY, upText.c_str(), true, EpdFontFamily::REGULAR); renderer.drawText(kGuideFontId, downX, textY, downText.c_str(), true, EpdFontFamily::REGULAR);
renderer.setOrientation(origOrientation);}