Add UI scaling support for touch vs button devices
Introduces uiScale() method to apply per-board UI scaling based on device type (1.0 for button devices, >1 for touch devices to make UI elements finger-sized). Adds scaledMetrics member to store scaled theme metrics that are returned by getMetrics().
This commit is contained in:
@@ -100,6 +100,23 @@ void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) {
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::setUiFontRemap(const int* from, const int* to, int count) {
|
||||
if (count < 0) count = 0;
|
||||
if (count > MAX_UI_FONT_REMAP) count = MAX_UI_FONT_REMAP;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
uiFontFrom_[i] = from[i];
|
||||
uiFontTo_[i] = to[i];
|
||||
}
|
||||
uiFontRemapCount_ = count;
|
||||
}
|
||||
|
||||
int GfxRenderer::remapUiFont(const int fontId) const {
|
||||
for (int i = 0; i < uiFontRemapCount_; ++i) {
|
||||
if (uiFontFrom_[i] == fontId) return uiFontTo_[i];
|
||||
}
|
||||
return fontId;
|
||||
}
|
||||
|
||||
// Translate logical (x,y) coordinates to physical panel coordinates based on current orientation
|
||||
// This should always be inlined for better performance
|
||||
static inline void rotateCoordinates(const GfxRenderer::Orientation orientation, const int x, const int y, int* phyX,
|
||||
@@ -365,7 +382,7 @@ int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontF
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
@@ -407,7 +424,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
||||
return;
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return;
|
||||
@@ -1419,7 +1436,7 @@ int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style styl
|
||||
return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
@@ -1440,7 +1457,7 @@ int GfxRenderer::getSpaceAdvance(const int fontId, const uint32_t leftCp, const
|
||||
return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) return 0;
|
||||
const auto& font = fontIt->second;
|
||||
const EpdGlyph* spaceGlyph = font.getGlyph(' ', style);
|
||||
@@ -1454,7 +1471,7 @@ int GfxRenderer::getSpaceAdvance(const int fontId, const uint32_t leftCp, const
|
||||
|
||||
int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint32_t rightCp,
|
||||
const EpdFontFamily::Style style) const {
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) return 0;
|
||||
const int kernFP = fontIt->second.getKerning(leftCp, rightCp, style); // 4.4 fixed-point
|
||||
return fp4::toPixel(kernFP); // snap 4.4 fixed-point to nearest pixel
|
||||
@@ -1476,7 +1493,7 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
||||
return fp4::toPixel(widthFP);
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
@@ -1512,7 +1529,7 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
||||
}
|
||||
|
||||
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
@@ -1522,7 +1539,7 @@ int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||
}
|
||||
|
||||
int GfxRenderer::getLineHeight(const int fontId) const {
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
@@ -1532,7 +1549,7 @@ int GfxRenderer::getLineHeight(const int fontId) const {
|
||||
}
|
||||
|
||||
int GfxRenderer::getTextHeight(const int fontId) const {
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
@@ -1540,6 +1557,31 @@ int GfxRenderer::getTextHeight(const int fontId) const {
|
||||
return fontIt->second.getData(EpdFontFamily::REGULAR)->ascender;
|
||||
}
|
||||
|
||||
// Height of a reference glyph above the baseline (glyph->top). 'H' gives the real
|
||||
// cap height, 'x' the x-height — the actual visible extents of the font, for
|
||||
// vertical centering that follows the font instead of a guessed ascender fraction.
|
||||
int GfxRenderer::glyphTop(const int fontId, const uint32_t cp) const {
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) return 0;
|
||||
const EpdGlyph* g = fontIt->second.getGlyph(cp, EpdFontFamily::REGULAR);
|
||||
return g ? g->top : 0;
|
||||
}
|
||||
|
||||
int GfxRenderer::getFontCapHeight(const int fontId) const { return glyphTop(fontId, 'H'); }
|
||||
int GfxRenderer::getFontXHeight(const int fontId) const { return glyphTop(fontId, 'x'); }
|
||||
|
||||
int GfxRenderer::getTextVisualCenterOffset(const int fontId) const {
|
||||
// Distance from drawText's y (text top) down to the text's optical middle. The
|
||||
// baseline is at y + ascender; the lowercase mass is centered at half the
|
||||
// x-height above it, so the optical center is ascender - xHeight/2 below the top.
|
||||
// x-height-based (UI text is lowercase-heavy); falls back to ascender*0.7 if the
|
||||
// 'x' glyph is missing. Scales with the font, so no per-size tweaking.
|
||||
const int ascender = getFontAscenderSize(fontId);
|
||||
const int xHeight = getFontXHeight(fontId);
|
||||
if (xHeight <= 0) return (ascender * 7) / 10;
|
||||
return ascender - xHeight / 2;
|
||||
}
|
||||
|
||||
void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y, const char* text, const bool black,
|
||||
const EpdFontFamily::Style style) const {
|
||||
// Cannot draw a NULL / empty string
|
||||
@@ -1547,7 +1589,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
return;
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return;
|
||||
|
||||
@@ -51,6 +51,13 @@ class GfxRenderer {
|
||||
uint32_t frameBufferSize = HalDisplay::BUFFER_SIZE;
|
||||
std::vector<uint8_t*> bwBufferChunks;
|
||||
std::map<int, EpdFontFamily> fontMap;
|
||||
// UI chrome font remap table (see setUiFontRemap). Empty = identity.
|
||||
static constexpr int MAX_UI_FONT_REMAP = 8;
|
||||
int uiFontFrom_[MAX_UI_FONT_REMAP] = {0};
|
||||
int uiFontTo_[MAX_UI_FONT_REMAP] = {0};
|
||||
int uiFontRemapCount_ = 0;
|
||||
int remapUiFont(int fontId) const;
|
||||
int glyphTop(int fontId, uint32_t cp) const;
|
||||
// Mutable because ensureSdCardFontReady() is const (called from layout code
|
||||
// that holds a const GfxRenderer&) but triggers SD card reads and heap
|
||||
// allocation inside the SdCardFont objects. Same pragmatic compromise as
|
||||
@@ -95,6 +102,11 @@ class GfxRenderer {
|
||||
// Setup
|
||||
void begin(); // must be called right after display.begin()
|
||||
void insertFont(int fontId, EpdFontFamily font);
|
||||
// UI chrome font scaling: firmware supplies a small remap table (from the board
|
||||
// uiScale) that substitutes a larger font for each scaled UI font id at lookup
|
||||
// time, so layout and drawing stay consistent with no call-site changes. Reader
|
||||
// body fonts aren't in the table, so book text is unaffected. count <= 8.
|
||||
void setUiFontRemap(const int* from, const int* to, int count);
|
||||
// Clears both the flash-font map and any SD-font registration for fontId.
|
||||
// Coupled to avoid dangling SdCardFont* in sdCardFonts_ when callers free
|
||||
// the underlying SdCardFont and forget the SD-side unregister.
|
||||
@@ -207,6 +219,14 @@ class GfxRenderer {
|
||||
int getTextAdvanceX(int fontId, const char* text, EpdFontFamily::Style style) const;
|
||||
int getFontAscenderSize(int fontId) const;
|
||||
int getLineHeight(int fontId) const;
|
||||
// Real visible font metrics (from the 'H' / 'x' glyph geometry) for vertical
|
||||
// alignment that follows the font, not a guessed ascender fraction.
|
||||
int getFontCapHeight(int fontId) const;
|
||||
int getFontXHeight(int fontId) const;
|
||||
// Offset from drawText's y (text top) to the text's optical vertical center.
|
||||
// Center any element (icon, etc.) on a line of text via:
|
||||
// centerY = textTop + getTextVisualCenterOffset(fontId)
|
||||
int getTextVisualCenterOffset(int fontId) const;
|
||||
std::string truncatedText(int fontId, const char* text, int maxWidth,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
/// Word-wrap \p text into at most \p maxLines lines, each no wider than
|
||||
|
||||
@@ -249,8 +249,14 @@ bool HalGPIO::wasTouchDown(float& nx, float& ny) const { return inputMgr.wasTouc
|
||||
|
||||
unsigned long HalGPIO::lastTouchHeldMs() const { return inputMgr.lastTouchHeldMs(); }
|
||||
|
||||
bool HalGPIO::wasSwipe(float& nxStart, float& nyStart, float& nxEnd, float& nyEnd) const {
|
||||
return inputMgr.wasSwipe(nxStart, nyStart, nxEnd, nyEnd);
|
||||
}
|
||||
|
||||
bool HalGPIO::hasTouch() const { return inputMgr.hasTouch(); }
|
||||
|
||||
bool HalGPIO::wasTouchActivity() const { return inputMgr.wasTouchActivity(); }
|
||||
|
||||
bool HalGPIO::isXteinkDevice() const {
|
||||
const auto board = BoardConfig::ACTIVE.board;
|
||||
return board == BoardConfig::Board::XteinkX3 || board == BoardConfig::Board::XteinkX4;
|
||||
|
||||
@@ -90,9 +90,18 @@ class HalGPIO {
|
||||
// release frame (alongside wasTouchTap). For tap-vs-long-press decisions.
|
||||
unsigned long lastTouchHeldMs() const;
|
||||
|
||||
// Swipe (flick) gesture on the release frame: writes the start/end positions
|
||||
// normalized 0..1 in the panel's native frame (map via GfxRenderer::tapToLogical).
|
||||
// A swipe also raises wasTouchTap(), so check this first. False on non-touch.
|
||||
bool wasSwipe(float& nxStart, float& nyStart, float& nxEnd, float& nyEnd) const;
|
||||
|
||||
// True if a touch controller is present/active (runtime gate; false on the C3).
|
||||
bool hasTouch() const;
|
||||
|
||||
// True if a touch press or release happened this frame (the touch analogue of
|
||||
// wasAnyPressed/Released), for resetting idle/sleep timers and CPU frequency.
|
||||
bool wasTouchActivity() const;
|
||||
|
||||
// Setup wake up GPIO and enter deep sleep
|
||||
void startDeepSleep();
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include "CrossPointSettings.h"
|
||||
#include "components/TouchRegistry.h"
|
||||
|
||||
@@ -116,6 +118,22 @@ bool MappedInputManager::wasCoverTapped(int& id) const {
|
||||
return TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Cover, id);
|
||||
}
|
||||
|
||||
MappedInputManager::SwipeDir MappedInputManager::wasSwipe() const {
|
||||
float nxs = 0.0f, nys = 0.0f, nxe = 0.0f, nye = 0.0f;
|
||||
if (!gpio.wasSwipe(nxs, nys, nxe, nye)) return SwipeDir::None;
|
||||
// Map both endpoints into the logical frame so the direction follows what the
|
||||
// user sees regardless of panel mount/orientation.
|
||||
int sx = 0, sy = 0, ex = 0, ey = 0;
|
||||
renderer.tapToLogical(nxs, nys, sx, sy);
|
||||
renderer.tapToLogical(nxe, nye, ex, ey);
|
||||
const int dx = ex - sx;
|
||||
const int dy = ey - sy;
|
||||
if (std::abs(dx) >= std::abs(dy)) {
|
||||
return dx < 0 ? SwipeDir::Left : SwipeDir::Right;
|
||||
}
|
||||
return dy < 0 ? SwipeDir::Up : SwipeDir::Down;
|
||||
}
|
||||
|
||||
bool MappedInputManager::wasPressed(const Button button) const {
|
||||
// A top-left tap fires on the release frame; expose it on Back's press edge too
|
||||
// so menus that act on wasPressed(Back) also respond. Deliberately NOT folded
|
||||
|
||||
@@ -7,6 +7,7 @@ class GfxRenderer;
|
||||
class MappedInputManager {
|
||||
public:
|
||||
enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward };
|
||||
enum class SwipeDir { None, Left, Right, Up, Down };
|
||||
|
||||
struct Labels {
|
||||
const char* btn1;
|
||||
@@ -37,6 +38,9 @@ class MappedInputManager {
|
||||
// (id = item index). Distinct kinds so a screen with both doesn't confuse them.
|
||||
bool wasTabTapped(int& id) const;
|
||||
bool wasCoverTapped(int& id) const;
|
||||
// Swipe direction in the current logical (oriented) frame, or None. A swipe also
|
||||
// raises the tap helpers above, so check this first and consume it.
|
||||
SwipeDir wasSwipe() const;
|
||||
bool wasAnyPressed() const;
|
||||
bool wasAnyReleased() const;
|
||||
unsigned long getHeldTime() const;
|
||||
|
||||
@@ -268,11 +268,13 @@ void HomeActivity::render(RenderLock&&) {
|
||||
menuIcons.insert(menuIcons.begin(), Book);
|
||||
}
|
||||
|
||||
// Menu fills the space between the cover and the bottom reserve (button hints
|
||||
// when shown, else a margin). Sizing it to the real remaining height lets
|
||||
// drawButtonMenu fit the items, so a scaled-up menu never runs off the bottom.
|
||||
const int menuY = metrics.homeTopPadding + metrics.homeCoverTileHeight + metrics.homeMenuTopOffset;
|
||||
const int bottomReserve = (BaseTheme::showButtonHints() ? metrics.buttonHintsHeight : 0) + metrics.verticalSpacing;
|
||||
GUI.drawButtonMenu(
|
||||
renderer,
|
||||
Rect{0, metrics.homeTopPadding + metrics.homeCoverTileHeight + metrics.homeMenuTopOffset, pageWidth,
|
||||
pageHeight - (metrics.headerHeight + metrics.homeTopPadding + metrics.verticalSpacing +
|
||||
metrics.homeMenuTopOffset + metrics.buttonHintsHeight)},
|
||||
renderer, Rect{0, menuY, pageWidth, pageHeight - menuY - bottomReserve},
|
||||
static_cast<int>(menuItems.size()),
|
||||
metrics.homeContinueReadingInMenu ? selectorIndex : selectorIndex - recentBooks.size(),
|
||||
[&menuItems](int index) { return std::string(menuItems[index]); },
|
||||
|
||||
@@ -115,17 +115,34 @@ void SettingsActivity::onExit() {
|
||||
void SettingsActivity::loop() {
|
||||
bool hasChangedCategory = false;
|
||||
|
||||
// A horizontal swipe pages categories (and the tab bar slides to keep the new one
|
||||
// visible). Handled first, and it suppresses the row/tab tap handlers below so the
|
||||
// swipe's release isn't also read as a tap.
|
||||
const MappedInputManager::SwipeDir swipe = mappedInput.wasSwipe();
|
||||
const bool swiped = swipe != MappedInputManager::SwipeDir::None;
|
||||
if (swipe == MappedInputManager::SwipeDir::Left) {
|
||||
selectedCategoryIndex = ButtonNavigator::nextIndex(selectedCategoryIndex, categoryCount);
|
||||
selectedSettingIndex = 0;
|
||||
hasChangedCategory = true;
|
||||
requestUpdate();
|
||||
} else if (swipe == MappedInputManager::SwipeDir::Right) {
|
||||
selectedCategoryIndex = ButtonNavigator::previousIndex(selectedCategoryIndex, categoryCount);
|
||||
selectedSettingIndex = 0;
|
||||
hasChangedCategory = true;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
// Tap a settings row to select + activate it. Row 0 is the tab bar, so the list
|
||||
// is drawn at selectedSettingIndex - 1; map the tapped row back by +1. Touch-down
|
||||
// shows it selected; release toggles/activates below.
|
||||
int downId = -1;
|
||||
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < settingsCount) {
|
||||
if (!swiped && mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < settingsCount) {
|
||||
selectedSettingIndex = downId + 1;
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
int tappedId = -1;
|
||||
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < settingsCount) {
|
||||
if (!swiped && mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < settingsCount) {
|
||||
selectedSettingIndex = tappedId + 1;
|
||||
toggleCurrentSetting();
|
||||
requestUpdate();
|
||||
@@ -135,7 +152,7 @@ void SettingsActivity::loop() {
|
||||
// A tap on a category tab switches to it (keeps focus on the tab row); the
|
||||
// hasChangedCategory block below swaps in that category's settings list.
|
||||
int tabId = -1;
|
||||
if (mappedInput.wasTabTapped(tabId) && tabId >= 0 && tabId < categoryCount) {
|
||||
if (!swiped && mappedInput.wasTabTapped(tabId) && tabId >= 0 && tabId < categoryCount) {
|
||||
selectedCategoryIndex = tabId;
|
||||
selectedSettingIndex = 0;
|
||||
hasChangedCategory = true;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "UITheme.h"
|
||||
|
||||
#include <BoardConfig.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <memory>
|
||||
|
||||
#include "MappedInputManager.h"
|
||||
@@ -15,6 +17,66 @@
|
||||
|
||||
UITheme UITheme::instance;
|
||||
|
||||
float UITheme::uiScale() { return BoardConfig::ACTIVE.uiScale; }
|
||||
|
||||
namespace {
|
||||
// Round a pixel dimension by the UI scale.
|
||||
int sp(int v, float s) { return static_cast<int>(std::lround(v * s)); }
|
||||
} // namespace
|
||||
|
||||
ThemeMetrics scaleThemeMetrics(const ThemeMetrics& b, float s) {
|
||||
ThemeMetrics m = b;
|
||||
if (s == 1.0f) return m;
|
||||
m.batteryWidth = sp(b.batteryWidth, s);
|
||||
m.batteryHeight = sp(b.batteryHeight, s);
|
||||
m.topPadding = sp(b.topPadding, s);
|
||||
m.batteryBarHeight = sp(b.batteryBarHeight, s);
|
||||
m.headerHeight = sp(b.headerHeight, s);
|
||||
m.verticalSpacing = sp(b.verticalSpacing, s);
|
||||
m.contentSidePadding = sp(b.contentSidePadding, s);
|
||||
m.listRowHeight = sp(b.listRowHeight, s);
|
||||
m.listWithSubtitleRowHeight = sp(b.listWithSubtitleRowHeight, s);
|
||||
m.menuRowHeight = sp(b.menuRowHeight, s);
|
||||
m.menuSpacing = sp(b.menuSpacing, s);
|
||||
m.tabSpacing = sp(b.tabSpacing, s);
|
||||
m.tabBarHeight = sp(b.tabBarHeight, s);
|
||||
m.scrollBarWidth = sp(b.scrollBarWidth, s);
|
||||
m.scrollBarRightOffset = sp(b.scrollBarRightOffset, s);
|
||||
m.homeTopPadding = sp(b.homeTopPadding, s);
|
||||
// homeCoverHeight / homeCoverTileHeight are intentionally NOT scaled: the home
|
||||
// screen fills the fixed panel height exactly, so scaling the decorative cover
|
||||
// would push the menu off the bottom. The cover stays native; the menu (a touch
|
||||
// target) scales and is fit into the remaining space by drawButtonMenu.
|
||||
m.homeMenuTopOffset = sp(b.homeMenuTopOffset, s);
|
||||
m.buttonHintsHeight = sp(b.buttonHintsHeight, s);
|
||||
m.sideButtonHintsWidth = sp(b.sideButtonHintsWidth, s);
|
||||
m.progressBarHeight = sp(b.progressBarHeight, s);
|
||||
m.progressBarMarginTop = sp(b.progressBarMarginTop, s);
|
||||
m.statusBarHorizontalMargin = sp(b.statusBarHorizontalMargin, s);
|
||||
m.statusBarVerticalMargin = sp(b.statusBarVerticalMargin, s);
|
||||
m.keyboardKeyWidth = sp(b.keyboardKeyWidth, s);
|
||||
m.keyboardKeyHeight = sp(b.keyboardKeyHeight, s);
|
||||
m.keyboardKeySpacing = sp(b.keyboardKeySpacing, s);
|
||||
m.keyboardBottomKeyHeight = sp(b.keyboardBottomKeyHeight, s);
|
||||
m.keyboardBottomKeySpacing = sp(b.keyboardBottomKeySpacing, s);
|
||||
m.keyboardVerticalOffset = sp(b.keyboardVerticalOffset, s);
|
||||
m.keyboardKeyCornerRadius = sp(b.keyboardKeyCornerRadius, s);
|
||||
m.keyboardSecondaryLabelRightPadding = sp(b.keyboardSecondaryLabelRightPadding, s);
|
||||
m.keyboardSecondaryLabelTopPadding = sp(b.keyboardSecondaryLabelTopPadding, s);
|
||||
m.keyboardMinArrowHeadSize = sp(b.keyboardMinArrowHeadSize, s);
|
||||
m.popupMarginX = sp(b.popupMarginX, s);
|
||||
m.popupMarginY = sp(b.popupMarginY, s);
|
||||
m.popupFrameThickness = sp(b.popupFrameThickness, s);
|
||||
m.popupCornerRadius = sp(b.popupCornerRadius, s);
|
||||
m.popupTextBaselineOffsetY = sp(b.popupTextBaselineOffsetY, s);
|
||||
m.popupProgressBarHeight = sp(b.popupProgressBarHeight, s);
|
||||
m.textFieldHorizontalPadding = sp(b.textFieldHorizontalPadding, s);
|
||||
m.textFieldNormalThickness = sp(b.textFieldNormalThickness, s);
|
||||
m.textFieldCursorThickness = sp(b.textFieldCursorThickness, s);
|
||||
m.textFieldLineEndOffset = sp(b.textFieldLineEndOffset, s);
|
||||
return m;
|
||||
}
|
||||
|
||||
UITheme::UITheme() {
|
||||
auto themeType = static_cast<CrossPointSettings::UI_THEME>(SETTINGS.uiTheme);
|
||||
setTheme(themeType);
|
||||
@@ -26,28 +88,31 @@ void UITheme::reload() {
|
||||
}
|
||||
|
||||
void UITheme::setTheme(CrossPointSettings::UI_THEME type) {
|
||||
const ThemeMetrics* base = &BaseMetrics::values;
|
||||
switch (type) {
|
||||
case CrossPointSettings::UI_THEME::CLASSIC:
|
||||
LOG_DBG("UI", "Using Classic theme");
|
||||
currentTheme = std::make_unique<BaseTheme>();
|
||||
currentMetrics = &BaseMetrics::values;
|
||||
base = &BaseMetrics::values;
|
||||
break;
|
||||
case CrossPointSettings::UI_THEME::LYRA:
|
||||
LOG_DBG("UI", "Using Lyra theme");
|
||||
currentTheme = std::make_unique<LyraTheme>();
|
||||
currentMetrics = &LyraMetrics::values;
|
||||
base = &LyraMetrics::values;
|
||||
break;
|
||||
case CrossPointSettings::UI_THEME::ROUNDEDRAFF:
|
||||
LOG_DBG("UI", "Using RoundedRaff theme");
|
||||
currentTheme = std::make_unique<RoundedRaffTheme>();
|
||||
currentMetrics = &RoundedRaffMetrics::values;
|
||||
base = &RoundedRaffMetrics::values;
|
||||
break;
|
||||
case CrossPointSettings::UI_THEME::LYRA_3_COVERS:
|
||||
LOG_DBG("UI", "Using Lyra 3 Covers theme");
|
||||
currentTheme = std::make_unique<Lyra3CoversTheme>();
|
||||
currentMetrics = &Lyra3CoversMetrics::values;
|
||||
base = &Lyra3CoversMetrics::values;
|
||||
break;
|
||||
}
|
||||
scaledMetrics = scaleThemeMetrics(*base, uiScale());
|
||||
currentMetrics = &scaledMetrics;
|
||||
}
|
||||
|
||||
int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints,
|
||||
|
||||
@@ -31,10 +31,22 @@ class UITheme {
|
||||
static int getStatusBarHeight();
|
||||
static int getProgressBarHeight();
|
||||
|
||||
// Per-board UI scale (BoardConfig::ACTIVE.uiScale): 1.0 on button devices, >1 on
|
||||
// touch devices so chrome is finger-sized. Applied to the metrics below.
|
||||
static float uiScale();
|
||||
|
||||
private:
|
||||
const ThemeMetrics* currentMetrics;
|
||||
// Scaled copy of the active theme's constexpr metrics (scaled by uiScale() in
|
||||
// setTheme()). currentMetrics points here so getMetrics() returns scaled values.
|
||||
ThemeMetrics scaledMetrics{};
|
||||
std::unique_ptr<BaseTheme> currentTheme;
|
||||
};
|
||||
|
||||
// Scale a theme's pixel-dimension metrics by `scale` (counts, percents, ratios,
|
||||
// and bools are left untouched). At scale 1.0 it is an exact copy (no drift).
|
||||
// Shared by UITheme (active-theme metrics) and each theme's own draw code.
|
||||
ThemeMetrics scaleThemeMetrics(const ThemeMetrics& base, float scale);
|
||||
|
||||
// Helper macro to access current theme
|
||||
#define GUI UITheme::getInstance().getTheme()
|
||||
|
||||
@@ -22,6 +22,14 @@ constexpr int homeMenuMargin = 20;
|
||||
constexpr int homeMarginTop = 30;
|
||||
constexpr int subtitleY = 738;
|
||||
|
||||
// This theme's metrics scaled by the board uiScale (lazily built once; uiScale is
|
||||
// fixed at boot). Runtime draw code reads these so the UI grows on touch/high-PPI
|
||||
// boards. constexpr button-hint locals keep BaseMetrics::values (hints are off on
|
||||
// touch, so they need no scaling and must stay compile-time).
|
||||
const ThemeMetrics& M() {
|
||||
static const ThemeMetrics m = scaleThemeMetrics(BaseMetrics::values, UITheme::uiScale());
|
||||
return m;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void BaseTheme::drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight) {
|
||||
@@ -101,7 +109,10 @@ void BaseTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const b
|
||||
if (showPercentage) {
|
||||
const auto percentageText = std::to_string(percentage) + "%";
|
||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str());
|
||||
renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - batteryPercentSpacing, rect.y, percentageText.c_str());
|
||||
// Vertically center the percentage on the battery icon: place the text so its
|
||||
// optical center (from the real font metrics) sits at the icon's center.
|
||||
const int textY = y + rect.height / 2 - renderer.getTextVisualCenterOffset(SMALL_FONT_ID);
|
||||
renderer.drawText(SMALL_FONT_ID, rect.x - textWidth - batteryPercentSpacing, textY, percentageText.c_str());
|
||||
}
|
||||
|
||||
const Rect iconRect{rect.x, y, rect.width, rect.height};
|
||||
@@ -253,7 +264,7 @@ void BaseTheme::drawSideButtonHintsImpl(const GfxRenderer& renderer, const char*
|
||||
}
|
||||
|
||||
int BaseTheme::getListPageItems(int contentHeight, bool hasSubtitle) const {
|
||||
int rowHeight = (hasSubtitle) ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight;
|
||||
int rowHeight = (hasSubtitle) ? M().listWithSubtitleRowHeight : M().listRowHeight;
|
||||
return contentHeight / rowHeight;
|
||||
}
|
||||
|
||||
@@ -264,7 +275,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
const std::function<std::string(int index)>& rowValue, bool highlightValue,
|
||||
const std::function<bool(int index)>& rowDimmed) const {
|
||||
int rowHeight =
|
||||
(rowSubtitle != nullptr) ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight;
|
||||
(rowSubtitle != nullptr) ? M().listWithSubtitleRowHeight : M().listRowHeight;
|
||||
int pageItems = rect.height / rowHeight;
|
||||
|
||||
const int totalPages = (itemCount + pageItems - 1) / pageItems;
|
||||
@@ -306,7 +317,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
const int itemY = rect.y + (i % pageItems) * rowHeight;
|
||||
TouchRegistry::getInstance().add(Rect{rect.x, itemY - 2, rect.width, rowHeight}, i, TouchRegistry::Item);
|
||||
|
||||
int rowTextWidth = contentWidth - BaseMetrics::values.contentSidePadding * 2;
|
||||
int rowTextWidth = contentWidth - M().contentSidePadding * 2;
|
||||
std::string valueText;
|
||||
if (rowValue != nullptr) {
|
||||
valueText = rowValue(i);
|
||||
@@ -321,13 +332,13 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
auto itemName = rowTitle(i);
|
||||
auto font = UI_10_FONT_ID;
|
||||
auto item = renderer.truncatedText(font, itemName.c_str(), rowTextWidth);
|
||||
renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding, itemY, item.c_str(), i != selectedIndex);
|
||||
renderer.drawText(font, rect.x + M().contentSidePadding, itemY, item.c_str(), i != selectedIndex);
|
||||
|
||||
// Apply checkerboard dither to create gray text effect for dimmed items
|
||||
if (rowDimmed && rowDimmed(i) && i != selectedIndex) {
|
||||
const int titleWidth = renderer.getTextWidth(font, item.c_str());
|
||||
const int lineH = renderer.getLineHeight(font);
|
||||
const int tx = rect.x + BaseMetrics::values.contentSidePadding;
|
||||
const int tx = rect.x + M().contentSidePadding;
|
||||
for (int py = itemY; py < itemY + lineH; py++)
|
||||
for (int px = tx; px < tx + titleWidth; px++)
|
||||
if ((px + py) % 2 == 0) renderer.drawPixel(px, py, false);
|
||||
@@ -337,7 +348,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
std::string subtitleText = rowSubtitle(i);
|
||||
if (!subtitleText.empty()) {
|
||||
auto subtitle = renderer.truncatedText(SMALL_FONT_ID, subtitleText.c_str(), rowTextWidth);
|
||||
renderer.drawText(SMALL_FONT_ID, rect.x + BaseMetrics::values.contentSidePadding, itemY + 22, subtitle.c_str(),
|
||||
renderer.drawText(SMALL_FONT_ID, rect.x + M().contentSidePadding, itemY + 22, subtitle.c_str(),
|
||||
i != selectedIndex);
|
||||
}
|
||||
}
|
||||
@@ -348,7 +359,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
if (rowSubtitle != nullptr) {
|
||||
valueY = itemY + 10;
|
||||
}
|
||||
renderer.drawText(UI_10_FONT_ID, rect.x + contentWidth - BaseMetrics::values.contentSidePadding - valueTextWidth,
|
||||
renderer.drawText(UI_10_FONT_ID, rect.x + contentWidth - M().contentSidePadding - valueTextWidth,
|
||||
valueY, valueText.c_str(), i != selectedIndex);
|
||||
}
|
||||
}
|
||||
@@ -361,30 +372,30 @@ void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
||||
// Hide last battery draw
|
||||
constexpr int maxBatteryWidth = 80;
|
||||
renderer.fillRect(rect.x + rect.width - maxBatteryWidth, rect.y + 5, maxBatteryWidth,
|
||||
BaseMetrics::values.batteryHeight + 10, false);
|
||||
M().batteryHeight + 10, false);
|
||||
|
||||
const bool showBatteryPercentage =
|
||||
SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS;
|
||||
// Position icon at right edge, drawBatteryRight will place text to the left
|
||||
const int batteryX = rect.x + rect.width - 12 - BaseMetrics::values.batteryWidth;
|
||||
const int batteryX = rect.x + rect.width - 12 - M().batteryWidth;
|
||||
drawBatteryRight(renderer,
|
||||
Rect{batteryX, rect.y + 5, BaseMetrics::values.batteryWidth, BaseMetrics::values.batteryHeight},
|
||||
Rect{batteryX, rect.y + 5, M().batteryWidth, M().batteryHeight},
|
||||
showBatteryPercentage);
|
||||
|
||||
if (title) {
|
||||
int padding = rect.width - batteryX + BaseMetrics::values.batteryWidth;
|
||||
int padding = rect.width - batteryX + M().batteryWidth;
|
||||
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title,
|
||||
rect.width - padding * 2 - BaseMetrics::values.contentSidePadding * 2,
|
||||
rect.width - padding * 2 - M().contentSidePadding * 2,
|
||||
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);
|
||||
SMALL_FONT_ID, subtitle, rect.width - M().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,
|
||||
rect.x + rect.width - M().contentSidePadding - truncatedSubtitleWidth, subtitleY,
|
||||
truncatedSubtitle.c_str(), true);
|
||||
}
|
||||
}
|
||||
@@ -392,55 +403,80 @@ void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
||||
void BaseTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, const char* rightLabel) const {
|
||||
constexpr int maxListValueWidth = 200;
|
||||
|
||||
int currentX = rect.x + BaseMetrics::values.contentSidePadding;
|
||||
int rightSpace = BaseMetrics::values.contentSidePadding;
|
||||
int currentX = rect.x + M().contentSidePadding;
|
||||
int rightSpace = M().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,
|
||||
renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - M().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);
|
||||
UI_12_FONT_ID, label, rect.width - M().contentSidePadding - rightSpace, EpdFontFamily::REGULAR);
|
||||
renderer.drawText(UI_12_FONT_ID, currentX, rect.y, truncatedLabel.c_str(), true, EpdFontFamily::REGULAR);
|
||||
}
|
||||
|
||||
int BaseTheme::tabBarScrollOffset(int contentWidth, int selStart, int selWidth, int availWidth) {
|
||||
if (contentWidth <= availWidth) return 0; // fits, no scroll
|
||||
int offset = selStart - (availWidth - selWidth) / 2; // center the selected tab
|
||||
const int maxOffset = contentWidth - availWidth;
|
||||
if (offset < 0) offset = 0;
|
||||
if (offset > maxOffset) offset = maxOffset;
|
||||
return offset;
|
||||
}
|
||||
|
||||
void BaseTheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const std::vector<TabInfo>& tabs,
|
||||
bool selected) const {
|
||||
constexpr int underlineHeight = 2; // Height of selection underline
|
||||
constexpr int underlineGap = 4; // Gap between text and underline
|
||||
|
||||
const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
|
||||
const int pad = M().contentSidePadding;
|
||||
const int spacing = M().tabSpacing;
|
||||
|
||||
int currentX = rect.x + BaseMetrics::values.contentSidePadding;
|
||||
// Measure pass: total row width and the selected tab's position, so the row can
|
||||
// slide to keep the selected tab on screen when scaled tabs overflow the width.
|
||||
int contentWidth = 0, selStart = 0, selWidth = 0;
|
||||
for (const auto& tab : tabs) {
|
||||
const int w =
|
||||
renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
|
||||
if (tab.selected) {
|
||||
selStart = contentWidth;
|
||||
selWidth = w;
|
||||
}
|
||||
contentWidth += w + spacing;
|
||||
}
|
||||
const int scroll = tabBarScrollOffset(contentWidth, selStart, selWidth, rect.width - pad * 2);
|
||||
|
||||
int currentX = rect.x + pad - scroll;
|
||||
int tabIndex = 0;
|
||||
for (const auto& tab : tabs) {
|
||||
const int textWidth =
|
||||
renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
|
||||
const int idx = tabIndex++;
|
||||
|
||||
// Tap target spans the label plus the inter-tab gap, full bar height.
|
||||
TouchRegistry::getInstance().add(
|
||||
Rect{currentX - 3, rect.y, textWidth + BaseMetrics::values.tabSpacing, rect.height}, tabIndex++,
|
||||
TouchRegistry::Tab);
|
||||
// Skip tabs scrolled fully off the bar (don't register unreachable tap targets).
|
||||
if (currentX + textWidth > rect.x && currentX < rect.x + rect.width) {
|
||||
// Tap target spans the label plus the inter-tab gap, full bar height.
|
||||
TouchRegistry::getInstance().add(Rect{currentX - 3, rect.y, textWidth + spacing, rect.height}, idx,
|
||||
TouchRegistry::Tab);
|
||||
|
||||
// Draw underline for selected tab
|
||||
if (tab.selected) {
|
||||
if (selected) {
|
||||
renderer.fillRect(currentX - 3, rect.y, textWidth + 6, lineHeight + underlineGap);
|
||||
} else {
|
||||
renderer.fillRect(currentX, rect.y + lineHeight + underlineGap, textWidth, underlineHeight);
|
||||
if (tab.selected) {
|
||||
if (selected) {
|
||||
renderer.fillRect(currentX - 3, rect.y, textWidth + 6, lineHeight + underlineGap);
|
||||
} else {
|
||||
renderer.fillRect(currentX, rect.y + lineHeight + underlineGap, textWidth, underlineHeight);
|
||||
}
|
||||
}
|
||||
|
||||
renderer.drawText(UI_12_FONT_ID, currentX, rect.y, tab.label, !(tab.selected && selected),
|
||||
tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
|
||||
}
|
||||
|
||||
// Draw tab label
|
||||
renderer.drawText(UI_12_FONT_ID, currentX, rect.y, tab.label, !(tab.selected && selected),
|
||||
tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
|
||||
|
||||
currentX += textWidth + BaseMetrics::values.tabSpacing;
|
||||
currentX += textWidth + spacing;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,7 +499,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
if (hasContinueReading && !recentBooks[0].coverBmpPath.empty()) {
|
||||
// Try to get actual image dimensions from BMP header
|
||||
const std::string coverBmpPath =
|
||||
UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, BaseMetrics::values.homeCoverHeight);
|
||||
UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, M().homeCoverHeight);
|
||||
|
||||
HalFile file;
|
||||
if (Storage.openFileForRead("HOME", coverBmpPath, file)) {
|
||||
@@ -517,7 +553,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
|
||||
if (hasContinueReading && !recentBooks[0].coverBmpPath.empty() && !coverRendered) {
|
||||
const std::string coverBmpPath =
|
||||
UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, BaseMetrics::values.homeCoverHeight);
|
||||
UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, M().homeCoverHeight);
|
||||
|
||||
// First time: load cover from SD and render
|
||||
HalFile file;
|
||||
@@ -682,22 +718,27 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
void BaseTheme::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 {
|
||||
// Fit all buttons into rect.height (after the top pad): clamp the stride when the
|
||||
// scaled rows would overflow, so a scaled-up menu never runs off the bottom.
|
||||
const int topPad = M().verticalSpacing;
|
||||
const int sidePad = M().contentSidePadding;
|
||||
int stride = M().menuRowHeight + M().menuSpacing;
|
||||
if (buttonCount > 0 && topPad + stride * buttonCount > rect.height) {
|
||||
stride = (rect.height - topPad) / buttonCount;
|
||||
}
|
||||
const int rowHeight = std::max(1, stride - M().menuSpacing);
|
||||
|
||||
for (int i = 0; i < buttonCount; ++i) {
|
||||
const int tileY = BaseMetrics::values.verticalSpacing + rect.y +
|
||||
static_cast<int>(i) * (BaseMetrics::values.menuRowHeight + BaseMetrics::values.menuSpacing);
|
||||
TouchRegistry::getInstance().add(
|
||||
Rect{rect.x + BaseMetrics::values.contentSidePadding, tileY,
|
||||
rect.width - BaseMetrics::values.contentSidePadding * 2, BaseMetrics::values.menuRowHeight},
|
||||
i, TouchRegistry::Item);
|
||||
const int tileY = topPad + rect.y + static_cast<int>(i) * stride;
|
||||
TouchRegistry::getInstance().add(Rect{rect.x + sidePad, tileY, rect.width - sidePad * 2, rowHeight}, i,
|
||||
TouchRegistry::Item);
|
||||
|
||||
const bool selected = selectedIndex == i;
|
||||
|
||||
if (selected) {
|
||||
renderer.fillRect(rect.x + BaseMetrics::values.contentSidePadding, tileY,
|
||||
rect.width - BaseMetrics::values.contentSidePadding * 2, BaseMetrics::values.menuRowHeight);
|
||||
renderer.fillRect(rect.x + sidePad, tileY, rect.width - sidePad * 2, rowHeight);
|
||||
} else {
|
||||
renderer.drawRect(rect.x + BaseMetrics::values.contentSidePadding, tileY,
|
||||
rect.width - BaseMetrics::values.contentSidePadding * 2, BaseMetrics::values.menuRowHeight);
|
||||
renderer.drawRect(rect.x + sidePad, tileY, rect.width - sidePad * 2, rowHeight);
|
||||
}
|
||||
|
||||
std::string labelStr = buttonLabel(i);
|
||||
@@ -705,8 +746,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);
|
||||
}
|
||||
|
||||
@@ -231,6 +231,12 @@ class BaseTheme {
|
||||
bool inactiveSelection = false) const;
|
||||
virtual bool showsFileIcons() const { return false; }
|
||||
|
||||
// Horizontal scroll offset (px to subtract from tab X positions) that keeps the
|
||||
// selected tab visible when the tab row is wider than availWidth — used by every
|
||||
// theme's drawTabBar so scaled-up tabs slide instead of running off-screen.
|
||||
// Returns 0 when the whole row fits.
|
||||
static int tabBarScrollOffset(int contentWidth, int selStart, int selWidth, int availWidth);
|
||||
|
||||
// Shared constants and helpers for battery drawing (used by all themes)
|
||||
static constexpr int batteryPercentSpacing = 4;
|
||||
static void drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight);
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
|
||||
// Internal constants
|
||||
namespace {
|
||||
// This theme's metrics scaled by the board uiScale (see BaseTheme.cpp M()).
|
||||
const ThemeMetrics& M() {
|
||||
static const ThemeMetrics m = scaleThemeMetrics(Lyra3CoversMetrics::values, UITheme::uiScale());
|
||||
return m;
|
||||
}
|
||||
constexpr int hPaddingInSelection = 8;
|
||||
constexpr int cornerRadius = 6;
|
||||
} // namespace
|
||||
@@ -21,7 +26,7 @@ constexpr int cornerRadius = 6;
|
||||
void Lyra3CoversTheme::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 * Lyra3CoversMetrics::values.contentSidePadding) / 3;
|
||||
const int tileWidth = (rect.width - 2 * M().contentSidePadding) / 3;
|
||||
const int tileY = rect.y;
|
||||
const bool hasContinueReading = !recentBooks.empty();
|
||||
|
||||
@@ -31,15 +36,15 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
||||
if (hasContinueReading) {
|
||||
if (!coverRendered) {
|
||||
for (int i = 0;
|
||||
i < std::min(static_cast<int>(recentBooks.size()), Lyra3CoversMetrics::values.homeRecentBooksCount); i++) {
|
||||
i < std::min(static_cast<int>(recentBooks.size()), M().homeRecentBooksCount); i++) {
|
||||
std::string coverPath = recentBooks[i].coverBmpPath;
|
||||
bool hasCover = true;
|
||||
int tileX = Lyra3CoversMetrics::values.contentSidePadding + tileWidth * i;
|
||||
int tileX = M().contentSidePadding + tileWidth * i;
|
||||
if (coverPath.empty()) {
|
||||
hasCover = false;
|
||||
} else {
|
||||
const std::string coverBmpPath =
|
||||
UITheme::getCoverThumbPath(coverPath, Lyra3CoversMetrics::values.homeCoverHeight);
|
||||
UITheme::getCoverThumbPath(coverPath, M().homeCoverHeight);
|
||||
|
||||
// First time: load cover from SD and render
|
||||
HalFile file;
|
||||
@@ -50,11 +55,11 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
||||
float coverWidth = static_cast<float>(bitmap.getWidth());
|
||||
float ratio = coverWidth / coverHeight;
|
||||
const float tileRatio = static_cast<float>(tileWidth - 2 * hPaddingInSelection) /
|
||||
static_cast<float>(Lyra3CoversMetrics::values.homeCoverHeight);
|
||||
static_cast<float>(M().homeCoverHeight);
|
||||
float cropX = 1.0f - (tileRatio / ratio);
|
||||
|
||||
renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection,
|
||||
tileWidth - 2 * hPaddingInSelection, Lyra3CoversMetrics::values.homeCoverHeight,
|
||||
tileWidth - 2 * hPaddingInSelection, M().homeCoverHeight,
|
||||
cropX);
|
||||
} else {
|
||||
hasCover = false;
|
||||
@@ -64,13 +69,13 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
||||
}
|
||||
// Draw either way
|
||||
renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, tileWidth - 2 * hPaddingInSelection,
|
||||
Lyra3CoversMetrics::values.homeCoverHeight, true);
|
||||
M().homeCoverHeight, true);
|
||||
|
||||
if (!hasCover) {
|
||||
// Render empty cover
|
||||
renderer.fillRect(tileX + hPaddingInSelection,
|
||||
tileY + hPaddingInSelection + (Lyra3CoversMetrics::values.homeCoverHeight / 3),
|
||||
tileWidth - 2 * hPaddingInSelection, 2 * Lyra3CoversMetrics::values.homeCoverHeight / 3,
|
||||
tileY + hPaddingInSelection + (M().homeCoverHeight / 3),
|
||||
tileWidth - 2 * hPaddingInSelection, 2 * M().homeCoverHeight / 3,
|
||||
true);
|
||||
renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32, 32);
|
||||
}
|
||||
@@ -80,11 +85,11 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
||||
coverRendered = coverBufferStored; // Only consider it rendered if we successfully stored the buffer
|
||||
}
|
||||
|
||||
for (int i = 0; i < std::min(static_cast<int>(recentBooks.size()), Lyra3CoversMetrics::values.homeRecentBooksCount);
|
||||
for (int i = 0; i < std::min(static_cast<int>(recentBooks.size()), M().homeRecentBooksCount);
|
||||
i++) {
|
||||
bool bookSelected = (selectorIndex == i);
|
||||
|
||||
int tileX = Lyra3CoversMetrics::values.contentSidePadding + tileWidth * i;
|
||||
int tileX = M().contentSidePadding + tileWidth * i;
|
||||
|
||||
const int maxLineWidth = tileWidth - 2 * hPaddingInSelection;
|
||||
|
||||
@@ -100,15 +105,15 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
||||
renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false,
|
||||
Color::LightGray);
|
||||
renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection,
|
||||
Lyra3CoversMetrics::values.homeCoverHeight, Color::LightGray);
|
||||
M().homeCoverHeight, Color::LightGray);
|
||||
renderer.fillRectDither(tileX + tileWidth - hPaddingInSelection, tileY + hPaddingInSelection,
|
||||
hPaddingInSelection, Lyra3CoversMetrics::values.homeCoverHeight, Color::LightGray);
|
||||
renderer.fillRoundedRect(tileX, tileY + Lyra3CoversMetrics::values.homeCoverHeight + hPaddingInSelection,
|
||||
hPaddingInSelection, M().homeCoverHeight, Color::LightGray);
|
||||
renderer.fillRoundedRect(tileX, tileY + M().homeCoverHeight + hPaddingInSelection,
|
||||
tileWidth, dynamicTitleBoxHeight, cornerRadius, false, false, true, true,
|
||||
Color::LightGray);
|
||||
}
|
||||
|
||||
int currentY = tileY + Lyra3CoversMetrics::values.homeCoverHeight + hPaddingInSelection + 5;
|
||||
int currentY = tileY + M().homeCoverHeight + hPaddingInSelection + 5;
|
||||
for (const auto& line : titleLines) {
|
||||
renderer.drawText(SMALL_FONT_ID, tileX + hPaddingInSelection, currentY, line.c_str(), true);
|
||||
currentY += titleLineHeight;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -32,6 +33,11 @@
|
||||
|
||||
// Internal constants
|
||||
namespace {
|
||||
// This theme's metrics scaled by the board uiScale (see BaseTheme.cpp M()).
|
||||
const ThemeMetrics& M() {
|
||||
static const ThemeMetrics m = scaleThemeMetrics(LyraMetrics::values, UITheme::uiScale());
|
||||
return m;
|
||||
}
|
||||
constexpr int hPaddingInSelection = 8;
|
||||
constexpr int cornerRadius = 6;
|
||||
constexpr int topHintButtonY = 345;
|
||||
@@ -83,6 +89,14 @@ const uint8_t* iconForName(UIIcon icon, int size) {
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Top-left Y to box-center an icon on a line of text whose top is at textTop. The
|
||||
// UI icons are already centered in their boxes, so this just box-centers the icon
|
||||
// on the text's optical middle, which the renderer derives from the real font
|
||||
// metrics (x-height) — no per-size fraction to tweak.
|
||||
int iconYForText(const GfxRenderer& renderer, int fontId, int textTop, int boxSize) {
|
||||
return textTop + renderer.getTextVisualCenterOffset(fontId) - boxSize / 2;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void LyraTheme::fillBatteryIcon(const GfxRenderer& renderer, Rect rect, uint16_t percentage) const {
|
||||
@@ -112,9 +126,9 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
||||
const bool showBatteryPercentage =
|
||||
SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS;
|
||||
// Position icon at right edge, drawBatteryRight will place text to the left
|
||||
const int batteryX = rect.x + rect.width - 12 - LyraMetrics::values.batteryWidth;
|
||||
const int batteryX = rect.x + rect.width - 12 - M().batteryWidth;
|
||||
drawBatteryRight(renderer,
|
||||
Rect{batteryX, rect.y + 5, LyraMetrics::values.batteryWidth, LyraMetrics::values.batteryHeight},
|
||||
Rect{batteryX, rect.y + 5, M().batteryWidth, M().batteryHeight},
|
||||
showBatteryPercentage);
|
||||
|
||||
int maxTitleWidth = title != nullptr ? renderer.getTextWidth(UI_12_FONT_ID, title, EpdFontFamily::BOLD) : 0;
|
||||
@@ -122,7 +136,7 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
||||
subtitle != nullptr ? renderer.getTextWidth(SMALL_FONT_ID, subtitle, EpdFontFamily::REGULAR) : 0;
|
||||
|
||||
// Available space is the distance between the side paddings, and a with side padding between title and subtitle.
|
||||
const int availableSpace = rect.width - LyraMetrics::values.contentSidePadding * 3;
|
||||
const int availableSpace = rect.width - M().contentSidePadding * 3;
|
||||
|
||||
if (maxTitleWidth + maxSubtitleWidth > availableSpace) {
|
||||
if ((maxTitleWidth > availableSpace / 2) && (maxSubtitleWidth > availableSpace / 2)) {
|
||||
@@ -141,8 +155,8 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
||||
|
||||
if (title) {
|
||||
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title, maxTitleWidth, EpdFontFamily::BOLD);
|
||||
renderer.drawText(UI_12_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding,
|
||||
rect.y + LyraMetrics::values.batteryBarHeight + 3, truncatedTitle.c_str(), true,
|
||||
renderer.drawText(UI_12_FONT_ID, rect.x + M().contentSidePadding,
|
||||
rect.y + M().batteryBarHeight + 3, truncatedTitle.c_str(), true,
|
||||
EpdFontFamily::BOLD);
|
||||
renderer.drawLine(rect.x, rect.y + rect.height - 3, rect.x + rect.width - 1, rect.y + rect.height - 3, 3, true);
|
||||
}
|
||||
@@ -151,25 +165,25 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
||||
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.x + rect.width - M().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;
|
||||
int currentX = rect.x + M().contentSidePadding;
|
||||
int rightSpace = M().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,
|
||||
renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - M().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);
|
||||
UI_10_FONT_ID, label, rect.width - M().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 - 1, rect.y + rect.height - 1, true);
|
||||
@@ -177,43 +191,57 @@ void LyraTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char
|
||||
|
||||
void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs,
|
||||
bool selected) const {
|
||||
int currentX = rect.x + LyraMetrics::values.contentSidePadding;
|
||||
const int pad = M().contentSidePadding;
|
||||
const int spacing = M().tabSpacing;
|
||||
|
||||
if (selected) {
|
||||
renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, Color::LightGray);
|
||||
}
|
||||
|
||||
// Measure pass: slide the row to keep the selected tab visible when scaled tabs
|
||||
// overflow the width (slot = label + horizontal padding).
|
||||
int contentWidth = 0, selStart = 0, selWidth = 0;
|
||||
for (const auto& tab : tabs) {
|
||||
const int slotW = renderer.getTextWidth(UI_10_FONT_ID, tab.label, EpdFontFamily::REGULAR) + 2 * hPaddingInSelection;
|
||||
if (tab.selected) {
|
||||
selStart = contentWidth;
|
||||
selWidth = slotW;
|
||||
}
|
||||
contentWidth += slotW + spacing;
|
||||
}
|
||||
const int scroll = tabBarScrollOffset(contentWidth, selStart, selWidth, rect.width - pad * 2);
|
||||
|
||||
int currentX = rect.x + pad - scroll;
|
||||
int tabIndex = 0;
|
||||
for (const auto& tab : tabs) {
|
||||
const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, tab.label, EpdFontFamily::REGULAR);
|
||||
const int slotW = textWidth + 2 * hPaddingInSelection;
|
||||
const int idx = tabIndex++;
|
||||
|
||||
TouchRegistry::getInstance().add(
|
||||
Rect{currentX, rect.y, textWidth + 2 * hPaddingInSelection + LyraMetrics::values.tabSpacing, rect.height},
|
||||
tabIndex++, TouchRegistry::Tab);
|
||||
if (currentX + slotW > rect.x && currentX < rect.x + rect.width) {
|
||||
TouchRegistry::getInstance().add(Rect{currentX, rect.y, slotW + spacing, rect.height}, idx, TouchRegistry::Tab);
|
||||
|
||||
if (tab.selected) {
|
||||
if (selected) {
|
||||
renderer.fillRoundedRect(currentX, rect.y + 1, textWidth + 2 * hPaddingInSelection, rect.height - 4,
|
||||
cornerRadius, Color::Black);
|
||||
} else {
|
||||
renderer.fillRectDither(currentX, rect.y, textWidth + 2 * hPaddingInSelection, rect.height - 3,
|
||||
Color::LightGray);
|
||||
renderer.drawLine(currentX, rect.y + rect.height - 3, currentX + textWidth + 2 * hPaddingInSelection,
|
||||
rect.y + rect.height - 3, 2, true);
|
||||
if (tab.selected) {
|
||||
if (selected) {
|
||||
renderer.fillRoundedRect(currentX, rect.y + 1, slotW, rect.height - 4, cornerRadius, Color::Black);
|
||||
} else {
|
||||
renderer.fillRectDither(currentX, rect.y, slotW, rect.height - 3, Color::LightGray);
|
||||
renderer.drawLine(currentX, rect.y + rect.height - 3, currentX + slotW, rect.y + rect.height - 3, 2, true);
|
||||
}
|
||||
}
|
||||
|
||||
renderer.drawText(UI_10_FONT_ID, currentX + hPaddingInSelection, rect.y + 6, tab.label,
|
||||
!(tab.selected && selected), EpdFontFamily::REGULAR);
|
||||
}
|
||||
|
||||
renderer.drawText(UI_10_FONT_ID, currentX + hPaddingInSelection, rect.y + 6, tab.label, !(tab.selected && selected),
|
||||
EpdFontFamily::REGULAR);
|
||||
|
||||
currentX += textWidth + LyraMetrics::values.tabSpacing + 2 * hPaddingInSelection;
|
||||
currentX += slotW + spacing;
|
||||
}
|
||||
|
||||
renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width - 1, rect.y + rect.height - 1, true);
|
||||
}
|
||||
|
||||
int LyraTheme::getListPageItems(int contentHeight, bool hasSubtitle) const {
|
||||
int rowHeight = (hasSubtitle) ? LyraMetrics::values.listWithSubtitleRowHeight : LyraMetrics::values.listRowHeight;
|
||||
int rowHeight = (hasSubtitle) ? M().listWithSubtitleRowHeight : M().listRowHeight;
|
||||
return contentHeight / rowHeight;
|
||||
}
|
||||
|
||||
@@ -224,7 +252,7 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
const std::function<std::string(int index)>& rowValue, bool highlightValue,
|
||||
const std::function<bool(int index)>& rowDimmed) const {
|
||||
int rowHeight =
|
||||
(rowSubtitle != nullptr) ? LyraMetrics::values.listWithSubtitleRowHeight : LyraMetrics::values.listRowHeight;
|
||||
(rowSubtitle != nullptr) ? M().listWithSubtitleRowHeight : M().listRowHeight;
|
||||
int pageItems = rect.height / rowHeight;
|
||||
|
||||
const int totalPages = (itemCount + pageItems - 1) / pageItems;
|
||||
@@ -235,34 +263,40 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
const int scrollBarHeight = (scrollAreaHeight * pageItems) / itemCount;
|
||||
const int currentPage = selectedIndex / pageItems;
|
||||
const int scrollBarY = rect.y + ((scrollAreaHeight - scrollBarHeight) * currentPage) / (totalPages - 1);
|
||||
const int scrollBarX = rect.x + rect.width - LyraMetrics::values.scrollBarRightOffset;
|
||||
const int scrollBarX = rect.x + rect.width - M().scrollBarRightOffset;
|
||||
renderer.drawLine(scrollBarX, rect.y, scrollBarX, rect.y + scrollAreaHeight, true);
|
||||
renderer.fillRect(scrollBarX - LyraMetrics::values.scrollBarWidth, scrollBarY, LyraMetrics::values.scrollBarWidth,
|
||||
renderer.fillRect(scrollBarX - M().scrollBarWidth, scrollBarY, M().scrollBarWidth,
|
||||
scrollBarHeight, true);
|
||||
}
|
||||
|
||||
// Draw selection
|
||||
int contentWidth =
|
||||
rect.width -
|
||||
(totalPages > 1 ? (LyraMetrics::values.scrollBarWidth + LyraMetrics::values.scrollBarRightOffset) : 1);
|
||||
(totalPages > 1 ? (M().scrollBarWidth + M().scrollBarRightOffset) : 1);
|
||||
if (selectedIndex >= 0) {
|
||||
renderer.fillRoundedRect(
|
||||
rect.x + LyraMetrics::values.contentSidePadding, rect.y + selectedIndex % pageItems * rowHeight,
|
||||
contentWidth - LyraMetrics::values.contentSidePadding * 2, rowHeight, cornerRadius, Color::LightGray);
|
||||
rect.x + M().contentSidePadding, rect.y + selectedIndex % pageItems * rowHeight,
|
||||
contentWidth - M().contentSidePadding * 2, rowHeight, cornerRadius, Color::LightGray);
|
||||
}
|
||||
|
||||
int textX = rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection;
|
||||
int textWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2;
|
||||
int iconSize;
|
||||
int textX = rect.x + M().contentSidePadding + hPaddingInSelection;
|
||||
int textWidth = contentWidth - M().contentSidePadding * 2 - hPaddingInSelection * 2;
|
||||
int iconSize = 0;
|
||||
if (rowIcon != nullptr) {
|
||||
iconSize = (rowSubtitle != nullptr) ? mainMenuIconSize : listIconSize;
|
||||
textX += iconSize + hPaddingInSelection;
|
||||
textWidth -= iconSize + hPaddingInSelection;
|
||||
}
|
||||
|
||||
// Title baseline-top offset within the row. Single-line rows center the title
|
||||
// (and its icon) vertically in the scaled row via the real font metrics, so the
|
||||
// row never looks top-heavy as it scales. Rows with a subtitle keep the stacked
|
||||
// layout (title near the top, subtitle below).
|
||||
const int titleOffset =
|
||||
(rowSubtitle != nullptr) ? 7 : rowHeight / 2 - renderer.getTextVisualCenterOffset(UI_10_FONT_ID);
|
||||
|
||||
// Draw all items
|
||||
const auto pageStartIndex = selectedIndex / pageItems * pageItems;
|
||||
int iconY = (rowSubtitle != nullptr) ? 16 : 10;
|
||||
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) {
|
||||
const int itemY = rect.y + (i % pageItems) * rowHeight;
|
||||
TouchRegistry::getInstance().add(Rect{rect.x, itemY, rect.width, rowHeight}, i, TouchRegistry::Item);
|
||||
@@ -278,15 +312,16 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
rowTextWidth -= valueWidth;
|
||||
}
|
||||
|
||||
const int titleTop = itemY + titleOffset;
|
||||
auto itemName = rowTitle(i);
|
||||
auto item = renderer.truncatedText(UI_10_FONT_ID, itemName.c_str(), rowTextWidth);
|
||||
renderer.drawText(UI_10_FONT_ID, textX, itemY + 7, item.c_str(), true);
|
||||
renderer.drawText(UI_10_FONT_ID, textX, titleTop, item.c_str(), true);
|
||||
|
||||
// Apply checkerboard dither to create gray text effect for dimmed items
|
||||
if (rowDimmed && rowDimmed(i) && i != selectedIndex) {
|
||||
const int titleWidth = renderer.getTextWidth(UI_10_FONT_ID, item.c_str());
|
||||
const int lineH = renderer.getLineHeight(UI_10_FONT_ID);
|
||||
for (int py = itemY + 7; py < itemY + 7 + lineH; py++)
|
||||
for (int py = titleTop; py < titleTop + lineH; py++)
|
||||
for (int px = textX; px < textX + titleWidth; px++)
|
||||
if ((px + py) % 2 == 0) renderer.drawPixel(px, py, false);
|
||||
}
|
||||
@@ -295,8 +330,9 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
UIIcon icon = rowIcon(i);
|
||||
const uint8_t* iconBitmap = iconForName(icon, iconSize);
|
||||
if (iconBitmap != nullptr) {
|
||||
renderer.drawIcon(iconBitmap, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection,
|
||||
itemY + iconY, iconSize, iconSize);
|
||||
const int iconDrawY = iconYForText(renderer, UI_10_FONT_ID, titleTop, iconSize);
|
||||
renderer.drawIcon(iconBitmap, rect.x + M().contentSidePadding + hPaddingInSelection, iconDrawY, iconSize,
|
||||
iconSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +347,7 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
if (!valueText.empty()) {
|
||||
if (i == selectedIndex && highlightValue) {
|
||||
renderer.fillRoundedRect(
|
||||
rect.x + contentWidth - LyraMetrics::values.contentSidePadding - hPaddingInSelection - valueWidth, itemY,
|
||||
rect.x + contentWidth - M().contentSidePadding - hPaddingInSelection - valueWidth, itemY,
|
||||
valueWidth + hPaddingInSelection, rowHeight, cornerRadius, Color::Black);
|
||||
}
|
||||
|
||||
@@ -319,7 +355,7 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
if (rowSubtitle != nullptr) {
|
||||
valueY = itemY + 16;
|
||||
}
|
||||
renderer.drawText(UI_10_FONT_ID, rect.x + contentWidth - LyraMetrics::values.contentSidePadding - valueWidth,
|
||||
renderer.drawText(UI_10_FONT_ID, rect.x + contentWidth - M().contentSidePadding - valueWidth,
|
||||
valueY, valueText.c_str(), !(i == selectedIndex && highlightValue));
|
||||
}
|
||||
}
|
||||
@@ -416,20 +452,20 @@ void LyraTheme::drawSideButtonHintsImpl(const GfxRenderer& renderer, const char*
|
||||
void LyraTheme::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 * LyraMetrics::values.contentSidePadding;
|
||||
const int tileWidth = rect.width - 2 * M().contentSidePadding;
|
||||
const int tileHeight = rect.height;
|
||||
const int tileY = rect.y;
|
||||
const bool hasContinueReading = !recentBooks.empty();
|
||||
if (coverWidth == 0) {
|
||||
coverWidth = LyraMetrics::values.homeCoverHeight * 0.6;
|
||||
coverWidth = M().homeCoverHeight * 0.6;
|
||||
}
|
||||
|
||||
// Tapping anywhere on the continue-reading card (cover + title/gray area) opens
|
||||
// recentBooks[0] (home selector 0) — full card width, not just the cover photo.
|
||||
if (hasContinueReading) {
|
||||
TouchRegistry::getInstance().add(
|
||||
Rect{LyraMetrics::values.contentSidePadding, tileY, tileWidth,
|
||||
LyraMetrics::values.homeCoverHeight + 2 * hPaddingInSelection},
|
||||
Rect{M().contentSidePadding, tileY, tileWidth,
|
||||
M().homeCoverHeight + 2 * hPaddingInSelection},
|
||||
0, TouchRegistry::Cover);
|
||||
}
|
||||
|
||||
@@ -441,11 +477,11 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
if (!coverRendered) {
|
||||
std::string coverPath = book.coverBmpPath;
|
||||
bool hasCover = true;
|
||||
int tileX = LyraMetrics::values.contentSidePadding;
|
||||
int tileX = M().contentSidePadding;
|
||||
if (coverPath.empty()) {
|
||||
hasCover = false;
|
||||
} else {
|
||||
const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::values.homeCoverHeight);
|
||||
const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, M().homeCoverHeight);
|
||||
|
||||
// First time: load cover from SD and render
|
||||
HalFile file;
|
||||
@@ -454,7 +490,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);
|
||||
M().homeCoverHeight);
|
||||
} else {
|
||||
hasCover = false;
|
||||
}
|
||||
@@ -464,13 +500,13 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
|
||||
// Draw either way
|
||||
renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth,
|
||||
LyraMetrics::values.homeCoverHeight, true);
|
||||
M().homeCoverHeight, true);
|
||||
|
||||
if (!hasCover) {
|
||||
// Render empty cover
|
||||
renderer.fillRect(tileX + hPaddingInSelection,
|
||||
tileY + hPaddingInSelection + (LyraMetrics::values.homeCoverHeight / 3), coverWidth,
|
||||
2 * LyraMetrics::values.homeCoverHeight / 3, true);
|
||||
tileY + hPaddingInSelection + (M().homeCoverHeight / 3), coverWidth,
|
||||
2 * M().homeCoverHeight / 3, true);
|
||||
renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32, 32);
|
||||
}
|
||||
|
||||
@@ -480,19 +516,19 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
|
||||
bool bookSelected = (selectorIndex == 0);
|
||||
|
||||
int tileX = LyraMetrics::values.contentSidePadding;
|
||||
int textWidth = tileWidth - 2 * hPaddingInSelection - LyraMetrics::values.verticalSpacing - coverWidth;
|
||||
int tileX = M().contentSidePadding;
|
||||
int textWidth = tileWidth - 2 * hPaddingInSelection - M().verticalSpacing - coverWidth;
|
||||
|
||||
if (bookSelected) {
|
||||
// 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);
|
||||
M().homeCoverHeight, Color::LightGray);
|
||||
renderer.fillRectDither(tileX + hPaddingInSelection + coverWidth, tileY + hPaddingInSelection,
|
||||
tileWidth - hPaddingInSelection - coverWidth, LyraMetrics::values.homeCoverHeight,
|
||||
tileWidth - hPaddingInSelection - coverWidth, M().homeCoverHeight,
|
||||
Color::LightGray);
|
||||
renderer.fillRoundedRect(tileX, tileY + LyraMetrics::values.homeCoverHeight + hPaddingInSelection, tileWidth,
|
||||
renderer.fillRoundedRect(tileX, tileY + M().homeCoverHeight + hPaddingInSelection, tileWidth,
|
||||
hPaddingInSelection, cornerRadius, false, false, true, true, Color::LightGray);
|
||||
}
|
||||
|
||||
@@ -504,7 +540,7 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
||||
const int authorHeight = book.author.empty() ? 0 : (renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2);
|
||||
const int totalBlockHeight = titleBlockHeight + authorHeight;
|
||||
int titleY = tileY + tileHeight / 2 - totalBlockHeight / 2;
|
||||
const int textX = tileX + hPaddingInSelection + coverWidth + LyraMetrics::values.verticalSpacing;
|
||||
const int textX = tileX + hPaddingInSelection + coverWidth + M().verticalSpacing;
|
||||
for (const auto& line : titleLines) {
|
||||
renderer.drawText(UI_12_FONT_ID, textX, titleY, line.c_str(), true, EpdFontFamily::BOLD);
|
||||
titleY += titleLineHeight;
|
||||
@@ -529,11 +565,17 @@ void LyraTheme::drawEmptyRecents(const GfxRenderer& renderer, const Rect rect) c
|
||||
void LyraTheme::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 {
|
||||
// Fit all buttons into rect.height: clamp the stride (and row height) when the
|
||||
// scaled rows would overflow, so a scaled-up menu never runs off the bottom.
|
||||
int stride = M().menuRowHeight + M().menuSpacing;
|
||||
if (buttonCount > 0 && stride * buttonCount > rect.height) {
|
||||
stride = rect.height / buttonCount;
|
||||
}
|
||||
const int rowHeight = std::max(1, stride - M().menuSpacing);
|
||||
|
||||
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};
|
||||
int tileWidth = rect.width - M().contentSidePadding * 2;
|
||||
Rect tileRect = Rect{rect.x + M().contentSidePadding, rect.y + i * stride, tileWidth, rowHeight};
|
||||
TouchRegistry::getInstance().add(tileRect, i, TouchRegistry::Item);
|
||||
|
||||
const bool selected = selectedIndex == i;
|
||||
@@ -546,13 +588,14 @@ 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);
|
||||
const uint8_t* iconBitmap = iconForName(icon, mainMenuIconSize);
|
||||
if (iconBitmap != nullptr) {
|
||||
renderer.drawIcon(iconBitmap, textX, textY + 3, mainMenuIconSize, mainMenuIconSize);
|
||||
const int iconY = iconYForText(renderer, UI_12_FONT_ID, textY, mainMenuIconSize);
|
||||
renderer.drawIcon(iconBitmap, textX, iconY, mainMenuIconSize, mainMenuIconSize);
|
||||
textX += mainMenuIconSize + hPaddingInSelection + 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ constexpr ThemeMetrics values = {.batteryWidth = 16,
|
||||
.listRowHeight = 40,
|
||||
.listWithSubtitleRowHeight = 60,
|
||||
.menuRowHeight = 64,
|
||||
.menuSpacing = 8,
|
||||
.menuSpacing = 14,
|
||||
.tabSpacing = 8,
|
||||
.tabBarHeight = 40,
|
||||
.scrollBarWidth = 4,
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
// This theme's metrics scaled by the board uiScale (see BaseTheme.cpp M()).
|
||||
const ThemeMetrics& M() {
|
||||
static const ThemeMetrics m = scaleThemeMetrics(RoundedRaffMetrics::values, UITheme::uiScale());
|
||||
return m;
|
||||
}
|
||||
constexpr int kCoverRadius = 18;
|
||||
constexpr int kMenuRadius = 30;
|
||||
constexpr int kBottomRadius = 15;
|
||||
@@ -30,8 +35,8 @@ void drawScrollBar(const GfxRenderer& renderer, Rect rect, int itemCount, int pa
|
||||
return;
|
||||
}
|
||||
|
||||
const int barW = RoundedRaffMetrics::values.scrollBarWidth;
|
||||
const int barX = rect.x + rect.width - RoundedRaffMetrics::values.scrollBarRightOffset - barW;
|
||||
const int barW = M().scrollBarWidth;
|
||||
const int barX = rect.x + rect.width - M().scrollBarRightOffset - barW;
|
||||
const int barY = rect.y;
|
||||
const int barH = rect.height;
|
||||
|
||||
@@ -54,13 +59,13 @@ void RoundedRaffTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const
|
||||
if (title == nullptr) {
|
||||
return;
|
||||
}
|
||||
const int sidePadding = RoundedRaffMetrics::values.contentSidePadding;
|
||||
const int sidePadding = M().contentSidePadding;
|
||||
const int titleX = rect.x + sidePadding;
|
||||
const int titleY = rect.y + 14;
|
||||
|
||||
const bool showBatteryPercentage =
|
||||
SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS;
|
||||
const int batteryIconX = rect.x + rect.width - sidePadding - RoundedRaffMetrics::values.batteryWidth;
|
||||
const int batteryIconX = rect.x + rect.width - sidePadding - M().batteryWidth;
|
||||
|
||||
// Reserve space for the widest possible percentage text to avoid title/battery overlap
|
||||
int batteryGroupLeftX = batteryIconX;
|
||||
@@ -69,8 +74,8 @@ void RoundedRaffTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const
|
||||
const int maxTextWidth = renderer.getTextWidth(SMALL_FONT_ID, "100%");
|
||||
batteryGroupLeftX -= maxTextWidth + batteryPercentSpacing;
|
||||
|
||||
const int clearW = maxTextWidth + batteryPercentSpacing + RoundedRaffMetrics::values.batteryWidth;
|
||||
const int clearH = std::max(renderer.getTextHeight(SMALL_FONT_ID), RoundedRaffMetrics::values.batteryHeight + 8);
|
||||
const int clearW = maxTextWidth + batteryPercentSpacing + M().batteryWidth;
|
||||
const int clearH = std::max(renderer.getTextHeight(SMALL_FONT_ID), M().batteryHeight + 8);
|
||||
renderer.fillRect(batteryIconX - maxTextWidth - batteryPercentSpacing, rect.y + 14, clearW, clearH, false);
|
||||
}
|
||||
|
||||
@@ -78,8 +83,8 @@ void RoundedRaffTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const
|
||||
auto headerTitle = renderer.truncatedText(kTitleFontId, title, maxTitleWidth, EpdFontFamily::BOLD);
|
||||
renderer.drawText(kTitleFontId, titleX, titleY, headerTitle.c_str(), true, EpdFontFamily::BOLD);
|
||||
drawBatteryRight(renderer,
|
||||
Rect{batteryIconX, rect.y + 14, RoundedRaffMetrics::values.batteryWidth,
|
||||
RoundedRaffMetrics::values.batteryHeight},
|
||||
Rect{batteryIconX, rect.y + 14, M().batteryWidth,
|
||||
M().batteryHeight},
|
||||
showBatteryPercentage);
|
||||
}
|
||||
|
||||
@@ -119,20 +124,20 @@ void RoundedRaffTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const
|
||||
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 tileWidth = rect.width - 2 * M().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;
|
||||
coverWidth = M().homeCoverHeight * 0.6;
|
||||
}
|
||||
const int imgY = tileY + (tileHeight - RoundedRaffMetrics::values.homeCoverHeight) / 2;
|
||||
const int tileX = RoundedRaffMetrics::values.contentSidePadding;
|
||||
const int imgY = tileY + (tileHeight - M().homeCoverHeight) / 2;
|
||||
const int tileX = M().contentSidePadding;
|
||||
|
||||
// Tapping the continue-reading cover opens recentBooks[0] (home selector 0).
|
||||
if (hasContinueReading) {
|
||||
TouchRegistry::getInstance().add(
|
||||
Rect{tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth, RoundedRaffMetrics::values.homeCoverHeight}, 0,
|
||||
Rect{tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth, M().homeCoverHeight}, 0,
|
||||
TouchRegistry::Cover);
|
||||
}
|
||||
|
||||
@@ -148,7 +153,7 @@ void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
||||
hasCover = false;
|
||||
} else {
|
||||
const std::string coverBmpPath =
|
||||
UITheme::getCoverThumbPath(coverPath, RoundedRaffMetrics::values.homeCoverHeight);
|
||||
UITheme::getCoverThumbPath(coverPath, M().homeCoverHeight);
|
||||
|
||||
// First time: load cover from SD and render
|
||||
HalFile file;
|
||||
@@ -157,9 +162,9 @@ void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||
coverWidth = bitmap.getWidth();
|
||||
renderer.drawBitmap(bitmap, tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth,
|
||||
RoundedRaffMetrics::values.homeCoverHeight);
|
||||
M().homeCoverHeight);
|
||||
renderer.maskRoundedRectOutsideCorners(tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth,
|
||||
RoundedRaffMetrics::values.homeCoverHeight, kCoverRadius,
|
||||
M().homeCoverHeight, kCoverRadius,
|
||||
Color::LightGray);
|
||||
} else {
|
||||
hasCover = false;
|
||||
@@ -170,15 +175,15 @@ void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
||||
|
||||
// Draw either way
|
||||
renderer.drawRoundedRect(tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth,
|
||||
RoundedRaffMetrics::values.homeCoverHeight, 1, kCoverRadius, true);
|
||||
M().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.fillRect(tileX + (tileWidth - coverWidth) / 2, imgY + (M().homeCoverHeight / 3),
|
||||
coverWidth, 2 * M().homeCoverHeight / 3, true);
|
||||
renderer.drawIcon(CoverIcon, tileX + (tileWidth - coverWidth) / 2 + 24, imgY + 24, 32, 32);
|
||||
renderer.maskRoundedRectOutsideCorners(tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth,
|
||||
RoundedRaffMetrics::values.homeCoverHeight, kCoverRadius,
|
||||
M().homeCoverHeight, kCoverRadius,
|
||||
Color::LightGray);
|
||||
}
|
||||
|
||||
@@ -188,12 +193,12 @@ void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
||||
|
||||
renderer.fillRoundedRect(tileX, tileY, tileWidth, imgY - tileY, kRowRadius, true, true, false, false,
|
||||
Color::LightGray);
|
||||
renderer.fillRectDither(tileX, imgY, (tileWidth - coverWidth) / 2, RoundedRaffMetrics::values.homeCoverHeight,
|
||||
renderer.fillRectDither(tileX, imgY, (tileWidth - coverWidth) / 2, M().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,
|
||||
M().homeCoverHeight, Color::LightGray);
|
||||
renderer.fillRoundedRect(tileX, imgY + M().homeCoverHeight, tileWidth,
|
||||
tileHeight - (imgY - tileY + M().homeCoverHeight), kRowRadius,
|
||||
false, false, true, true, Color::LightGray);
|
||||
} else {
|
||||
renderer.fillRoundedRect(tileX, tileY, tileWidth, tileHeight, kRowRadius, Color::LightGray);
|
||||
@@ -206,7 +211,7 @@ void RoundedRaffTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int butt
|
||||
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 sidePadding = M().contentSidePadding;
|
||||
const int rowX = rect.x + sidePadding;
|
||||
const int rowHeight = renderer.getLineHeight(kTitleFontId) + 20; // 10px top + 10px bottom
|
||||
const int rowGap = kSelectableRowGap;
|
||||
@@ -330,12 +335,12 @@ void RoundedRaffTheme::drawList(const GfxRenderer& renderer, Rect rect, int item
|
||||
constexpr int subtitleInterLineGap = 4;
|
||||
const int subtitleRowHeight =
|
||||
subtitleTopPadding + titleLineHeight + subtitleInterLineGap + subtitleLineHeight + subtitleBottomPadding;
|
||||
const int rowHeight = hasSubtitle ? subtitleRowHeight : RoundedRaffMetrics::values.listRowHeight;
|
||||
const int rowHeight = hasSubtitle ? subtitleRowHeight : M().listRowHeight;
|
||||
const int rowStep = rowHeight + kSelectableRowGap;
|
||||
const int pageItems = std::max(1, rect.height / rowStep);
|
||||
const int pageStartIndex = std::max(0, selectedIndex / pageItems) * pageItems;
|
||||
|
||||
const int sidePadding = RoundedRaffMetrics::values.contentSidePadding;
|
||||
const int sidePadding = M().contentSidePadding;
|
||||
const int rowX = rect.x + sidePadding;
|
||||
const int rowWidth = rect.width - sidePadding * 2;
|
||||
|
||||
@@ -404,7 +409,7 @@ void RoundedRaffTheme::drawButtonHintsImpl(GfxRenderer& renderer, const char* bt
|
||||
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 hintHeight = M().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;
|
||||
|
||||
+32
-2
@@ -1,5 +1,7 @@
|
||||
#include <Arduino.h>
|
||||
#include <Epub.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <FontCacheManager.h>
|
||||
#include <FontDecompressor.h>
|
||||
#include <GfxRenderer.h>
|
||||
@@ -297,6 +299,34 @@ void setupDisplayAndFonts(bool seamless = false) {
|
||||
renderer.insertFont(UI_12_FONT_ID, ui12FontFamily);
|
||||
renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
|
||||
|
||||
// UI chrome font scaling on high-density / touch boards: Ubuntu UI is only built
|
||||
// at 10/12pt, so remap the small UI fonts up the Noto Sans ladder to grow chrome
|
||||
// text with uiScale. Reader body fonts aren't remapped, so book text is unchanged.
|
||||
if (const float s = UITheme::uiScale(); s > 1.05f) {
|
||||
auto nearestNotoSans = [](float pt) -> int {
|
||||
const struct {
|
||||
float pt;
|
||||
int id;
|
||||
} sizes[] = {{12, NOTOSANS_12_FONT_ID},
|
||||
{14, NOTOSANS_14_FONT_ID},
|
||||
{16, NOTOSANS_16_FONT_ID},
|
||||
{18, NOTOSANS_18_FONT_ID}};
|
||||
int best = sizes[0].id;
|
||||
float bestDiff = 1e9f;
|
||||
for (const auto& z : sizes) {
|
||||
const float d = std::fabs(z.pt - pt);
|
||||
if (d < bestDiff) {
|
||||
bestDiff = d;
|
||||
best = z.id;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
};
|
||||
const int from[3] = {SMALL_FONT_ID, UI_10_FONT_ID, UI_12_FONT_ID};
|
||||
const int to[3] = {nearestNotoSans(8 * s), nearestNotoSans(10 * s), nearestNotoSans(12 * s)};
|
||||
renderer.setUiFontRemap(from, to, 3);
|
||||
}
|
||||
|
||||
// Discover and load SD card fonts
|
||||
sdFontSystem.begin(renderer);
|
||||
|
||||
@@ -531,10 +561,10 @@ void loop() {
|
||||
|
||||
// Check for any user activity (button press or release) or active background work
|
||||
static unsigned long lastActivityTime = millis();
|
||||
if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || halTiltSensor.hadActivity() ||
|
||||
if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || gpio.wasTouchActivity() || halTiltSensor.hadActivity() ||
|
||||
activityManager.preventAutoSleep()) {
|
||||
lastActivityTime = millis(); // Reset inactivity timer
|
||||
powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity
|
||||
powerManager.setPowerSaving(false); // Restore normal CPU frequency on user (button or touch) activity
|
||||
}
|
||||
|
||||
static bool screenshotButtonsReleased = true;
|
||||
|
||||
Reference in New Issue
Block a user