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
|
// Translate logical (x,y) coordinates to physical panel coordinates based on current orientation
|
||||||
// This should always be inlined for better performance
|
// This should always be inlined for better performance
|
||||||
static inline void rotateCoordinates(const GfxRenderer::Orientation orientation, const int x, const int y, int* phyX,
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto fontIt = fontMap.find(fontId);
|
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||||
if (fontIt == fontMap.end()) {
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -407,7 +424,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto fontIt = fontMap.find(fontId);
|
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||||
if (fontIt == fontMap.end()) {
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return;
|
return;
|
||||||
@@ -1419,7 +1436,7 @@ int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style styl
|
|||||||
return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
|
return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto fontIt = fontMap.find(fontId);
|
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||||
if (fontIt == fontMap.end()) {
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1440,7 +1457,7 @@ int GfxRenderer::getSpaceAdvance(const int fontId, const uint32_t leftCp, const
|
|||||||
return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
|
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;
|
if (fontIt == fontMap.end()) return 0;
|
||||||
const auto& font = fontIt->second;
|
const auto& font = fontIt->second;
|
||||||
const EpdGlyph* spaceGlyph = font.getGlyph(' ', style);
|
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,
|
int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint32_t rightCp,
|
||||||
const EpdFontFamily::Style style) const {
|
const EpdFontFamily::Style style) const {
|
||||||
const auto fontIt = fontMap.find(fontId);
|
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||||
if (fontIt == fontMap.end()) return 0;
|
if (fontIt == fontMap.end()) return 0;
|
||||||
const int kernFP = fontIt->second.getKerning(leftCp, rightCp, style); // 4.4 fixed-point
|
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
|
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);
|
return fp4::toPixel(widthFP);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto fontIt = fontMap.find(fontId);
|
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||||
if (fontIt == fontMap.end()) {
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1512,7 +1529,7 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
|||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||||
const auto fontIt = fontMap.find(fontId);
|
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||||
if (fontIt == fontMap.end()) {
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1522,7 +1539,7 @@ int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getLineHeight(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()) {
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1532,7 +1549,7 @@ int GfxRenderer::getLineHeight(const int fontId) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int GfxRenderer::getTextHeight(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()) {
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1540,6 +1557,31 @@ int GfxRenderer::getTextHeight(const int fontId) const {
|
|||||||
return fontIt->second.getData(EpdFontFamily::REGULAR)->ascender;
|
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,
|
void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y, const char* text, const bool black,
|
||||||
const EpdFontFamily::Style style) const {
|
const EpdFontFamily::Style style) const {
|
||||||
// Cannot draw a NULL / empty string
|
// Cannot draw a NULL / empty string
|
||||||
@@ -1547,7 +1589,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto fontIt = fontMap.find(fontId);
|
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||||
if (fontIt == fontMap.end()) {
|
if (fontIt == fontMap.end()) {
|
||||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ class GfxRenderer {
|
|||||||
uint32_t frameBufferSize = HalDisplay::BUFFER_SIZE;
|
uint32_t frameBufferSize = HalDisplay::BUFFER_SIZE;
|
||||||
std::vector<uint8_t*> bwBufferChunks;
|
std::vector<uint8_t*> bwBufferChunks;
|
||||||
std::map<int, EpdFontFamily> fontMap;
|
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
|
// Mutable because ensureSdCardFontReady() is const (called from layout code
|
||||||
// that holds a const GfxRenderer&) but triggers SD card reads and heap
|
// that holds a const GfxRenderer&) but triggers SD card reads and heap
|
||||||
// allocation inside the SdCardFont objects. Same pragmatic compromise as
|
// allocation inside the SdCardFont objects. Same pragmatic compromise as
|
||||||
@@ -95,6 +102,11 @@ class GfxRenderer {
|
|||||||
// Setup
|
// Setup
|
||||||
void begin(); // must be called right after display.begin()
|
void begin(); // must be called right after display.begin()
|
||||||
void insertFont(int fontId, EpdFontFamily font);
|
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.
|
// Clears both the flash-font map and any SD-font registration for fontId.
|
||||||
// Coupled to avoid dangling SdCardFont* in sdCardFonts_ when callers free
|
// Coupled to avoid dangling SdCardFont* in sdCardFonts_ when callers free
|
||||||
// the underlying SdCardFont and forget the SD-side unregister.
|
// 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 getTextAdvanceX(int fontId, const char* text, EpdFontFamily::Style style) const;
|
||||||
int getFontAscenderSize(int fontId) const;
|
int getFontAscenderSize(int fontId) const;
|
||||||
int getLineHeight(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,
|
std::string truncatedText(int fontId, const char* text, int maxWidth,
|
||||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||||
/// Word-wrap \p text into at most \p maxLines lines, each no wider than
|
/// 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(); }
|
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::hasTouch() const { return inputMgr.hasTouch(); }
|
||||||
|
|
||||||
|
bool HalGPIO::wasTouchActivity() const { return inputMgr.wasTouchActivity(); }
|
||||||
|
|
||||||
bool HalGPIO::isXteinkDevice() const {
|
bool HalGPIO::isXteinkDevice() const {
|
||||||
const auto board = BoardConfig::ACTIVE.board;
|
const auto board = BoardConfig::ACTIVE.board;
|
||||||
return board == BoardConfig::Board::XteinkX3 || board == BoardConfig::Board::XteinkX4;
|
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.
|
// release frame (alongside wasTouchTap). For tap-vs-long-press decisions.
|
||||||
unsigned long lastTouchHeldMs() const;
|
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).
|
// True if a touch controller is present/active (runtime gate; false on the C3).
|
||||||
bool hasTouch() const;
|
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
|
// Setup wake up GPIO and enter deep sleep
|
||||||
void startDeepSleep();
|
void startDeepSleep();
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
#include "CrossPointSettings.h"
|
#include "CrossPointSettings.h"
|
||||||
#include "components/TouchRegistry.h"
|
#include "components/TouchRegistry.h"
|
||||||
|
|
||||||
@@ -116,6 +118,22 @@ bool MappedInputManager::wasCoverTapped(int& id) const {
|
|||||||
return TouchRegistry::getInstance().hitTest(lx, ly, TouchRegistry::Cover, id);
|
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 {
|
bool MappedInputManager::wasPressed(const Button button) const {
|
||||||
// A top-left tap fires on the release frame; expose it on Back's press edge too
|
// 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
|
// so menus that act on wasPressed(Back) also respond. Deliberately NOT folded
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ class GfxRenderer;
|
|||||||
class MappedInputManager {
|
class MappedInputManager {
|
||||||
public:
|
public:
|
||||||
enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward };
|
enum class Button { Back, Confirm, Left, Right, Up, Down, Power, PageBack, PageForward };
|
||||||
|
enum class SwipeDir { None, Left, Right, Up, Down };
|
||||||
|
|
||||||
struct Labels {
|
struct Labels {
|
||||||
const char* btn1;
|
const char* btn1;
|
||||||
@@ -37,6 +38,9 @@ class MappedInputManager {
|
|||||||
// (id = item index). Distinct kinds so a screen with both doesn't confuse them.
|
// (id = item index). Distinct kinds so a screen with both doesn't confuse them.
|
||||||
bool wasTabTapped(int& id) const;
|
bool wasTabTapped(int& id) const;
|
||||||
bool wasCoverTapped(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 wasAnyPressed() const;
|
||||||
bool wasAnyReleased() const;
|
bool wasAnyReleased() const;
|
||||||
unsigned long getHeldTime() const;
|
unsigned long getHeldTime() const;
|
||||||
|
|||||||
@@ -268,11 +268,13 @@ void HomeActivity::render(RenderLock&&) {
|
|||||||
menuIcons.insert(menuIcons.begin(), Book);
|
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(
|
GUI.drawButtonMenu(
|
||||||
renderer,
|
renderer, Rect{0, menuY, pageWidth, pageHeight - menuY - bottomReserve},
|
||||||
Rect{0, metrics.homeTopPadding + metrics.homeCoverTileHeight + metrics.homeMenuTopOffset, pageWidth,
|
|
||||||
pageHeight - (metrics.headerHeight + metrics.homeTopPadding + metrics.verticalSpacing +
|
|
||||||
metrics.homeMenuTopOffset + metrics.buttonHintsHeight)},
|
|
||||||
static_cast<int>(menuItems.size()),
|
static_cast<int>(menuItems.size()),
|
||||||
metrics.homeContinueReadingInMenu ? selectorIndex : selectorIndex - recentBooks.size(),
|
metrics.homeContinueReadingInMenu ? selectorIndex : selectorIndex - recentBooks.size(),
|
||||||
[&menuItems](int index) { return std::string(menuItems[index]); },
|
[&menuItems](int index) { return std::string(menuItems[index]); },
|
||||||
|
|||||||
@@ -115,17 +115,34 @@ void SettingsActivity::onExit() {
|
|||||||
void SettingsActivity::loop() {
|
void SettingsActivity::loop() {
|
||||||
bool hasChangedCategory = false;
|
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
|
// 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
|
// is drawn at selectedSettingIndex - 1; map the tapped row back by +1. Touch-down
|
||||||
// shows it selected; release toggles/activates below.
|
// shows it selected; release toggles/activates below.
|
||||||
int downId = -1;
|
int downId = -1;
|
||||||
if (mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < settingsCount) {
|
if (!swiped && mappedInput.wasItemTouchedDown(downId) && downId >= 0 && downId < settingsCount) {
|
||||||
selectedSettingIndex = downId + 1;
|
selectedSettingIndex = downId + 1;
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
int tappedId = -1;
|
int tappedId = -1;
|
||||||
if (mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < settingsCount) {
|
if (!swiped && mappedInput.wasItemTapped(tappedId) && tappedId >= 0 && tappedId < settingsCount) {
|
||||||
selectedSettingIndex = tappedId + 1;
|
selectedSettingIndex = tappedId + 1;
|
||||||
toggleCurrentSetting();
|
toggleCurrentSetting();
|
||||||
requestUpdate();
|
requestUpdate();
|
||||||
@@ -135,7 +152,7 @@ void SettingsActivity::loop() {
|
|||||||
// A tap on a category tab switches to it (keeps focus on the tab row); the
|
// 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.
|
// hasChangedCategory block below swaps in that category's settings list.
|
||||||
int tabId = -1;
|
int tabId = -1;
|
||||||
if (mappedInput.wasTabTapped(tabId) && tabId >= 0 && tabId < categoryCount) {
|
if (!swiped && mappedInput.wasTabTapped(tabId) && tabId >= 0 && tabId < categoryCount) {
|
||||||
selectedCategoryIndex = tabId;
|
selectedCategoryIndex = tabId;
|
||||||
selectedSettingIndex = 0;
|
selectedSettingIndex = 0;
|
||||||
hasChangedCategory = true;
|
hasChangedCategory = true;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
#include "UITheme.h"
|
#include "UITheme.h"
|
||||||
|
|
||||||
|
#include <BoardConfig.h>
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
@@ -15,6 +17,66 @@
|
|||||||
|
|
||||||
UITheme UITheme::instance;
|
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() {
|
UITheme::UITheme() {
|
||||||
auto themeType = static_cast<CrossPointSettings::UI_THEME>(SETTINGS.uiTheme);
|
auto themeType = static_cast<CrossPointSettings::UI_THEME>(SETTINGS.uiTheme);
|
||||||
setTheme(themeType);
|
setTheme(themeType);
|
||||||
@@ -26,28 +88,31 @@ void UITheme::reload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void UITheme::setTheme(CrossPointSettings::UI_THEME type) {
|
void UITheme::setTheme(CrossPointSettings::UI_THEME type) {
|
||||||
|
const ThemeMetrics* base = &BaseMetrics::values;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case CrossPointSettings::UI_THEME::CLASSIC:
|
case CrossPointSettings::UI_THEME::CLASSIC:
|
||||||
LOG_DBG("UI", "Using Classic theme");
|
LOG_DBG("UI", "Using Classic theme");
|
||||||
currentTheme = std::make_unique<BaseTheme>();
|
currentTheme = std::make_unique<BaseTheme>();
|
||||||
currentMetrics = &BaseMetrics::values;
|
base = &BaseMetrics::values;
|
||||||
break;
|
break;
|
||||||
case CrossPointSettings::UI_THEME::LYRA:
|
case CrossPointSettings::UI_THEME::LYRA:
|
||||||
LOG_DBG("UI", "Using Lyra theme");
|
LOG_DBG("UI", "Using Lyra theme");
|
||||||
currentTheme = std::make_unique<LyraTheme>();
|
currentTheme = std::make_unique<LyraTheme>();
|
||||||
currentMetrics = &LyraMetrics::values;
|
base = &LyraMetrics::values;
|
||||||
break;
|
break;
|
||||||
case CrossPointSettings::UI_THEME::ROUNDEDRAFF:
|
case CrossPointSettings::UI_THEME::ROUNDEDRAFF:
|
||||||
LOG_DBG("UI", "Using RoundedRaff theme");
|
LOG_DBG("UI", "Using RoundedRaff theme");
|
||||||
currentTheme = std::make_unique<RoundedRaffTheme>();
|
currentTheme = std::make_unique<RoundedRaffTheme>();
|
||||||
currentMetrics = &RoundedRaffMetrics::values;
|
base = &RoundedRaffMetrics::values;
|
||||||
break;
|
break;
|
||||||
case CrossPointSettings::UI_THEME::LYRA_3_COVERS:
|
case CrossPointSettings::UI_THEME::LYRA_3_COVERS:
|
||||||
LOG_DBG("UI", "Using Lyra 3 Covers theme");
|
LOG_DBG("UI", "Using Lyra 3 Covers theme");
|
||||||
currentTheme = std::make_unique<Lyra3CoversTheme>();
|
currentTheme = std::make_unique<Lyra3CoversTheme>();
|
||||||
currentMetrics = &Lyra3CoversMetrics::values;
|
base = &Lyra3CoversMetrics::values;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
scaledMetrics = scaleThemeMetrics(*base, uiScale());
|
||||||
|
currentMetrics = &scaledMetrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints,
|
int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints,
|
||||||
|
|||||||
@@ -31,10 +31,22 @@ class UITheme {
|
|||||||
static int getStatusBarHeight();
|
static int getStatusBarHeight();
|
||||||
static int getProgressBarHeight();
|
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:
|
private:
|
||||||
const ThemeMetrics* currentMetrics;
|
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;
|
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
|
// Helper macro to access current theme
|
||||||
#define GUI UITheme::getInstance().getTheme()
|
#define GUI UITheme::getInstance().getTheme()
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ constexpr int homeMenuMargin = 20;
|
|||||||
constexpr int homeMarginTop = 30;
|
constexpr int homeMarginTop = 30;
|
||||||
constexpr int subtitleY = 738;
|
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
|
} // namespace
|
||||||
|
|
||||||
void BaseTheme::drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight) {
|
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) {
|
if (showPercentage) {
|
||||||
const auto percentageText = std::to_string(percentage) + "%";
|
const auto percentageText = std::to_string(percentage) + "%";
|
||||||
const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, percentageText.c_str());
|
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};
|
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 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;
|
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<std::string(int index)>& rowValue, bool highlightValue,
|
||||||
const std::function<bool(int index)>& rowDimmed) const {
|
const std::function<bool(int index)>& rowDimmed) const {
|
||||||
int rowHeight =
|
int rowHeight =
|
||||||
(rowSubtitle != nullptr) ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight;
|
(rowSubtitle != nullptr) ? M().listWithSubtitleRowHeight : M().listRowHeight;
|
||||||
int pageItems = rect.height / rowHeight;
|
int pageItems = rect.height / rowHeight;
|
||||||
|
|
||||||
const int totalPages = (itemCount + pageItems - 1) / pageItems;
|
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;
|
const int itemY = rect.y + (i % pageItems) * rowHeight;
|
||||||
TouchRegistry::getInstance().add(Rect{rect.x, itemY - 2, rect.width, rowHeight}, i, TouchRegistry::Item);
|
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;
|
std::string valueText;
|
||||||
if (rowValue != nullptr) {
|
if (rowValue != nullptr) {
|
||||||
valueText = rowValue(i);
|
valueText = rowValue(i);
|
||||||
@@ -321,13 +332,13 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
|||||||
auto itemName = rowTitle(i);
|
auto itemName = rowTitle(i);
|
||||||
auto font = UI_10_FONT_ID;
|
auto font = UI_10_FONT_ID;
|
||||||
auto item = renderer.truncatedText(font, itemName.c_str(), rowTextWidth);
|
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
|
// Apply checkerboard dither to create gray text effect for dimmed items
|
||||||
if (rowDimmed && rowDimmed(i) && i != selectedIndex) {
|
if (rowDimmed && rowDimmed(i) && i != selectedIndex) {
|
||||||
const int titleWidth = renderer.getTextWidth(font, item.c_str());
|
const int titleWidth = renderer.getTextWidth(font, item.c_str());
|
||||||
const int lineH = renderer.getLineHeight(font);
|
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 py = itemY; py < itemY + lineH; py++)
|
||||||
for (int px = tx; px < tx + titleWidth; px++)
|
for (int px = tx; px < tx + titleWidth; px++)
|
||||||
if ((px + py) % 2 == 0) renderer.drawPixel(px, py, false);
|
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);
|
std::string subtitleText = rowSubtitle(i);
|
||||||
if (!subtitleText.empty()) {
|
if (!subtitleText.empty()) {
|
||||||
auto subtitle = renderer.truncatedText(SMALL_FONT_ID, subtitleText.c_str(), rowTextWidth);
|
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);
|
i != selectedIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,7 +359,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
|||||||
if (rowSubtitle != nullptr) {
|
if (rowSubtitle != nullptr) {
|
||||||
valueY = itemY + 10;
|
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);
|
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
|
// Hide last battery draw
|
||||||
constexpr int maxBatteryWidth = 80;
|
constexpr int maxBatteryWidth = 80;
|
||||||
renderer.fillRect(rect.x + rect.width - maxBatteryWidth, rect.y + 5, maxBatteryWidth,
|
renderer.fillRect(rect.x + rect.width - maxBatteryWidth, rect.y + 5, maxBatteryWidth,
|
||||||
BaseMetrics::values.batteryHeight + 10, false);
|
M().batteryHeight + 10, false);
|
||||||
|
|
||||||
const bool showBatteryPercentage =
|
const bool showBatteryPercentage =
|
||||||
SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS;
|
SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS;
|
||||||
// Position icon at right edge, drawBatteryRight will place text to the left
|
// 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,
|
drawBatteryRight(renderer,
|
||||||
Rect{batteryX, rect.y + 5, BaseMetrics::values.batteryWidth, BaseMetrics::values.batteryHeight},
|
Rect{batteryX, rect.y + 5, M().batteryWidth, M().batteryHeight},
|
||||||
showBatteryPercentage);
|
showBatteryPercentage);
|
||||||
|
|
||||||
if (title) {
|
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,
|
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);
|
EpdFontFamily::BOLD);
|
||||||
renderer.drawCenteredText(UI_12_FONT_ID, rect.y + 5, truncatedTitle.c_str(), true, EpdFontFamily::BOLD);
|
renderer.drawCenteredText(UI_12_FONT_ID, rect.y + 5, truncatedTitle.c_str(), true, EpdFontFamily::BOLD);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subtitle) {
|
if (subtitle) {
|
||||||
auto truncatedSubtitle = renderer.truncatedText(
|
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());
|
int truncatedSubtitleWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedSubtitle.c_str());
|
||||||
renderer.drawText(SMALL_FONT_ID,
|
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);
|
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 {
|
void BaseTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, const char* rightLabel) const {
|
||||||
constexpr int maxListValueWidth = 200;
|
constexpr int maxListValueWidth = 200;
|
||||||
|
|
||||||
int currentX = rect.x + BaseMetrics::values.contentSidePadding;
|
int currentX = rect.x + M().contentSidePadding;
|
||||||
int rightSpace = BaseMetrics::values.contentSidePadding;
|
int rightSpace = M().contentSidePadding;
|
||||||
if (rightLabel) {
|
if (rightLabel) {
|
||||||
auto truncatedRightLabel =
|
auto truncatedRightLabel =
|
||||||
renderer.truncatedText(SMALL_FONT_ID, rightLabel, maxListValueWidth, EpdFontFamily::REGULAR);
|
renderer.truncatedText(SMALL_FONT_ID, rightLabel, maxListValueWidth, EpdFontFamily::REGULAR);
|
||||||
int rightLabelWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedRightLabel.c_str());
|
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());
|
rect.y + 7, truncatedRightLabel.c_str());
|
||||||
rightSpace += rightLabelWidth + 10;
|
rightSpace += rightLabelWidth + 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto truncatedLabel = renderer.truncatedText(
|
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);
|
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,
|
void BaseTheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const std::vector<TabInfo>& tabs,
|
||||||
bool selected) const {
|
bool selected) const {
|
||||||
constexpr int underlineHeight = 2; // Height of selection underline
|
constexpr int underlineHeight = 2; // Height of selection underline
|
||||||
constexpr int underlineGap = 4; // Gap between text and underline
|
constexpr int underlineGap = 4; // Gap between text and underline
|
||||||
|
|
||||||
const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
|
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;
|
int tabIndex = 0;
|
||||||
for (const auto& tab : tabs) {
|
for (const auto& tab : tabs) {
|
||||||
const int textWidth =
|
const int textWidth =
|
||||||
renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
|
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.
|
// Skip tabs scrolled fully off the bar (don't register unreachable tap targets).
|
||||||
TouchRegistry::getInstance().add(
|
if (currentX + textWidth > rect.x && currentX < rect.x + rect.width) {
|
||||||
Rect{currentX - 3, rect.y, textWidth + BaseMetrics::values.tabSpacing, rect.height}, tabIndex++,
|
// Tap target spans the label plus the inter-tab gap, full bar height.
|
||||||
TouchRegistry::Tab);
|
TouchRegistry::getInstance().add(Rect{currentX - 3, rect.y, textWidth + spacing, rect.height}, idx,
|
||||||
|
TouchRegistry::Tab);
|
||||||
|
|
||||||
// Draw underline for selected tab
|
if (tab.selected) {
|
||||||
if (tab.selected) {
|
if (selected) {
|
||||||
if (selected) {
|
renderer.fillRect(currentX - 3, rect.y, textWidth + 6, lineHeight + underlineGap);
|
||||||
renderer.fillRect(currentX - 3, rect.y, textWidth + 6, lineHeight + underlineGap);
|
} else {
|
||||||
} else {
|
renderer.fillRect(currentX, rect.y + lineHeight + underlineGap, textWidth, underlineHeight);
|
||||||
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
|
currentX += textWidth + spacing;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,7 +499,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
if (hasContinueReading && !recentBooks[0].coverBmpPath.empty()) {
|
if (hasContinueReading && !recentBooks[0].coverBmpPath.empty()) {
|
||||||
// Try to get actual image dimensions from BMP header
|
// Try to get actual image dimensions from BMP header
|
||||||
const std::string coverBmpPath =
|
const std::string coverBmpPath =
|
||||||
UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, BaseMetrics::values.homeCoverHeight);
|
UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, M().homeCoverHeight);
|
||||||
|
|
||||||
HalFile file;
|
HalFile file;
|
||||||
if (Storage.openFileForRead("HOME", coverBmpPath, 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) {
|
if (hasContinueReading && !recentBooks[0].coverBmpPath.empty() && !coverRendered) {
|
||||||
const std::string coverBmpPath =
|
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
|
// First time: load cover from SD and render
|
||||||
HalFile file;
|
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,
|
void BaseTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex,
|
||||||
const std::function<std::string(int index)>& buttonLabel,
|
const std::function<std::string(int index)>& buttonLabel,
|
||||||
const std::function<UIIcon(int index)>& rowIcon) const {
|
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) {
|
for (int i = 0; i < buttonCount; ++i) {
|
||||||
const int tileY = BaseMetrics::values.verticalSpacing + rect.y +
|
const int tileY = topPad + rect.y + static_cast<int>(i) * stride;
|
||||||
static_cast<int>(i) * (BaseMetrics::values.menuRowHeight + BaseMetrics::values.menuSpacing);
|
TouchRegistry::getInstance().add(Rect{rect.x + sidePad, tileY, rect.width - sidePad * 2, rowHeight}, i,
|
||||||
TouchRegistry::getInstance().add(
|
TouchRegistry::Item);
|
||||||
Rect{rect.x + BaseMetrics::values.contentSidePadding, tileY,
|
|
||||||
rect.width - BaseMetrics::values.contentSidePadding * 2, BaseMetrics::values.menuRowHeight},
|
|
||||||
i, TouchRegistry::Item);
|
|
||||||
|
|
||||||
const bool selected = selectedIndex == i;
|
const bool selected = selectedIndex == i;
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
renderer.fillRect(rect.x + BaseMetrics::values.contentSidePadding, tileY,
|
renderer.fillRect(rect.x + sidePad, tileY, rect.width - sidePad * 2, rowHeight);
|
||||||
rect.width - BaseMetrics::values.contentSidePadding * 2, BaseMetrics::values.menuRowHeight);
|
|
||||||
} else {
|
} else {
|
||||||
renderer.drawRect(rect.x + BaseMetrics::values.contentSidePadding, tileY,
|
renderer.drawRect(rect.x + sidePad, tileY, rect.width - sidePad * 2, rowHeight);
|
||||||
rect.width - BaseMetrics::values.contentSidePadding * 2, BaseMetrics::values.menuRowHeight);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string labelStr = buttonLabel(i);
|
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 textWidth = renderer.getTextWidth(UI_10_FONT_ID, label);
|
||||||
const int textX = rect.x + (rect.width - textWidth) / 2;
|
const int textX = rect.x + (rect.width - textWidth) / 2;
|
||||||
const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
|
const int lineHeight = renderer.getLineHeight(UI_10_FONT_ID);
|
||||||
const int textY =
|
const int textY = tileY + (rowHeight - lineHeight) / 2; // vertically centered assuming y is top of text
|
||||||
tileY + (BaseMetrics::values.menuRowHeight - lineHeight) / 2; // vertically centered assuming y is top of text
|
|
||||||
// Invert text when the tile is selected, to contrast with the filled background
|
// Invert text when the tile is selected, to contrast with the filled background
|
||||||
renderer.drawText(UI_10_FONT_ID, textX, textY, label, selectedIndex != i);
|
renderer.drawText(UI_10_FONT_ID, textX, textY, label, selectedIndex != i);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,6 +231,12 @@ class BaseTheme {
|
|||||||
bool inactiveSelection = false) const;
|
bool inactiveSelection = false) const;
|
||||||
virtual bool showsFileIcons() const { return false; }
|
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)
|
// Shared constants and helpers for battery drawing (used by all themes)
|
||||||
static constexpr int batteryPercentSpacing = 4;
|
static constexpr int batteryPercentSpacing = 4;
|
||||||
static void drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight);
|
static void drawBatteryOutline(const GfxRenderer& renderer, int x, int y, int battWidth, int rectHeight);
|
||||||
|
|||||||
@@ -14,6 +14,11 @@
|
|||||||
|
|
||||||
// Internal constants
|
// Internal constants
|
||||||
namespace {
|
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 hPaddingInSelection = 8;
|
||||||
constexpr int cornerRadius = 6;
|
constexpr int cornerRadius = 6;
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -21,7 +26,7 @@ constexpr int cornerRadius = 6;
|
|||||||
void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
||||||
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
||||||
bool& bufferRestored, std::function<bool()> storeCoverBuffer) const {
|
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 int tileY = rect.y;
|
||||||
const bool hasContinueReading = !recentBooks.empty();
|
const bool hasContinueReading = !recentBooks.empty();
|
||||||
|
|
||||||
@@ -31,15 +36,15 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
|||||||
if (hasContinueReading) {
|
if (hasContinueReading) {
|
||||||
if (!coverRendered) {
|
if (!coverRendered) {
|
||||||
for (int i = 0;
|
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;
|
std::string coverPath = recentBooks[i].coverBmpPath;
|
||||||
bool hasCover = true;
|
bool hasCover = true;
|
||||||
int tileX = Lyra3CoversMetrics::values.contentSidePadding + tileWidth * i;
|
int tileX = M().contentSidePadding + tileWidth * i;
|
||||||
if (coverPath.empty()) {
|
if (coverPath.empty()) {
|
||||||
hasCover = false;
|
hasCover = false;
|
||||||
} else {
|
} else {
|
||||||
const std::string coverBmpPath =
|
const std::string coverBmpPath =
|
||||||
UITheme::getCoverThumbPath(coverPath, Lyra3CoversMetrics::values.homeCoverHeight);
|
UITheme::getCoverThumbPath(coverPath, M().homeCoverHeight);
|
||||||
|
|
||||||
// First time: load cover from SD and render
|
// First time: load cover from SD and render
|
||||||
HalFile file;
|
HalFile file;
|
||||||
@@ -50,11 +55,11 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
|||||||
float coverWidth = static_cast<float>(bitmap.getWidth());
|
float coverWidth = static_cast<float>(bitmap.getWidth());
|
||||||
float ratio = coverWidth / coverHeight;
|
float ratio = coverWidth / coverHeight;
|
||||||
const float tileRatio = static_cast<float>(tileWidth - 2 * hPaddingInSelection) /
|
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);
|
float cropX = 1.0f - (tileRatio / ratio);
|
||||||
|
|
||||||
renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection,
|
renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection,
|
||||||
tileWidth - 2 * hPaddingInSelection, Lyra3CoversMetrics::values.homeCoverHeight,
|
tileWidth - 2 * hPaddingInSelection, M().homeCoverHeight,
|
||||||
cropX);
|
cropX);
|
||||||
} else {
|
} else {
|
||||||
hasCover = false;
|
hasCover = false;
|
||||||
@@ -64,13 +69,13 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
|||||||
}
|
}
|
||||||
// Draw either way
|
// Draw either way
|
||||||
renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, tileWidth - 2 * hPaddingInSelection,
|
renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, tileWidth - 2 * hPaddingInSelection,
|
||||||
Lyra3CoversMetrics::values.homeCoverHeight, true);
|
M().homeCoverHeight, true);
|
||||||
|
|
||||||
if (!hasCover) {
|
if (!hasCover) {
|
||||||
// Render empty cover
|
// Render empty cover
|
||||||
renderer.fillRect(tileX + hPaddingInSelection,
|
renderer.fillRect(tileX + hPaddingInSelection,
|
||||||
tileY + hPaddingInSelection + (Lyra3CoversMetrics::values.homeCoverHeight / 3),
|
tileY + hPaddingInSelection + (M().homeCoverHeight / 3),
|
||||||
tileWidth - 2 * hPaddingInSelection, 2 * Lyra3CoversMetrics::values.homeCoverHeight / 3,
|
tileWidth - 2 * hPaddingInSelection, 2 * M().homeCoverHeight / 3,
|
||||||
true);
|
true);
|
||||||
renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32, 32);
|
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
|
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++) {
|
i++) {
|
||||||
bool bookSelected = (selectorIndex == i);
|
bool bookSelected = (selectorIndex == i);
|
||||||
|
|
||||||
int tileX = Lyra3CoversMetrics::values.contentSidePadding + tileWidth * i;
|
int tileX = M().contentSidePadding + tileWidth * i;
|
||||||
|
|
||||||
const int maxLineWidth = tileWidth - 2 * hPaddingInSelection;
|
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,
|
renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false,
|
||||||
Color::LightGray);
|
Color::LightGray);
|
||||||
renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection,
|
renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection,
|
||||||
Lyra3CoversMetrics::values.homeCoverHeight, Color::LightGray);
|
M().homeCoverHeight, Color::LightGray);
|
||||||
renderer.fillRectDither(tileX + tileWidth - hPaddingInSelection, tileY + hPaddingInSelection,
|
renderer.fillRectDither(tileX + tileWidth - hPaddingInSelection, tileY + hPaddingInSelection,
|
||||||
hPaddingInSelection, Lyra3CoversMetrics::values.homeCoverHeight, Color::LightGray);
|
hPaddingInSelection, M().homeCoverHeight, Color::LightGray);
|
||||||
renderer.fillRoundedRect(tileX, tileY + Lyra3CoversMetrics::values.homeCoverHeight + hPaddingInSelection,
|
renderer.fillRoundedRect(tileX, tileY + M().homeCoverHeight + hPaddingInSelection,
|
||||||
tileWidth, dynamicTitleBoxHeight, cornerRadius, false, false, true, true,
|
tileWidth, dynamicTitleBoxHeight, cornerRadius, false, false, true, true,
|
||||||
Color::LightGray);
|
Color::LightGray);
|
||||||
}
|
}
|
||||||
|
|
||||||
int currentY = tileY + Lyra3CoversMetrics::values.homeCoverHeight + hPaddingInSelection + 5;
|
int currentY = tileY + M().homeCoverHeight + hPaddingInSelection + 5;
|
||||||
for (const auto& line : titleLines) {
|
for (const auto& line : titleLines) {
|
||||||
renderer.drawText(SMALL_FONT_ID, tileX + hPaddingInSelection, currentY, line.c_str(), true);
|
renderer.drawText(SMALL_FONT_ID, tileX + hPaddingInSelection, currentY, line.c_str(), true);
|
||||||
currentY += titleLineHeight;
|
currentY += titleLineHeight;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -32,6 +33,11 @@
|
|||||||
|
|
||||||
// Internal constants
|
// Internal constants
|
||||||
namespace {
|
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 hPaddingInSelection = 8;
|
||||||
constexpr int cornerRadius = 6;
|
constexpr int cornerRadius = 6;
|
||||||
constexpr int topHintButtonY = 345;
|
constexpr int topHintButtonY = 345;
|
||||||
@@ -83,6 +89,14 @@ const uint8_t* iconForName(UIIcon icon, int size) {
|
|||||||
}
|
}
|
||||||
return nullptr;
|
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
|
} // namespace
|
||||||
|
|
||||||
void LyraTheme::fillBatteryIcon(const GfxRenderer& renderer, Rect rect, uint16_t percentage) const {
|
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 =
|
const bool showBatteryPercentage =
|
||||||
SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS;
|
SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS;
|
||||||
// Position icon at right edge, drawBatteryRight will place text to the left
|
// 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,
|
drawBatteryRight(renderer,
|
||||||
Rect{batteryX, rect.y + 5, LyraMetrics::values.batteryWidth, LyraMetrics::values.batteryHeight},
|
Rect{batteryX, rect.y + 5, M().batteryWidth, M().batteryHeight},
|
||||||
showBatteryPercentage);
|
showBatteryPercentage);
|
||||||
|
|
||||||
int maxTitleWidth = title != nullptr ? renderer.getTextWidth(UI_12_FONT_ID, title, EpdFontFamily::BOLD) : 0;
|
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;
|
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.
|
// 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 + maxSubtitleWidth > availableSpace) {
|
||||||
if ((maxTitleWidth > availableSpace / 2) && (maxSubtitleWidth > availableSpace / 2)) {
|
if ((maxTitleWidth > availableSpace / 2) && (maxSubtitleWidth > availableSpace / 2)) {
|
||||||
@@ -141,8 +155,8 @@ void LyraTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* t
|
|||||||
|
|
||||||
if (title) {
|
if (title) {
|
||||||
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title, maxTitleWidth, EpdFontFamily::BOLD);
|
auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title, maxTitleWidth, EpdFontFamily::BOLD);
|
||||||
renderer.drawText(UI_12_FONT_ID, rect.x + LyraMetrics::values.contentSidePadding,
|
renderer.drawText(UI_12_FONT_ID, rect.x + M().contentSidePadding,
|
||||||
rect.y + LyraMetrics::values.batteryBarHeight + 3, truncatedTitle.c_str(), true,
|
rect.y + M().batteryBarHeight + 3, truncatedTitle.c_str(), true,
|
||||||
EpdFontFamily::BOLD);
|
EpdFontFamily::BOLD);
|
||||||
renderer.drawLine(rect.x, rect.y + rect.height - 3, rect.x + rect.width - 1, rect.y + rect.height - 3, 3, true);
|
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);
|
auto truncatedSubtitle = renderer.truncatedText(SMALL_FONT_ID, subtitle, maxSubtitleWidth, EpdFontFamily::REGULAR);
|
||||||
int truncatedSubtitleWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedSubtitle.c_str());
|
int truncatedSubtitleWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedSubtitle.c_str());
|
||||||
renderer.drawText(SMALL_FONT_ID,
|
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);
|
rect.y + 50, truncatedSubtitle.c_str(), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LyraTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, const char* rightLabel) const {
|
void LyraTheme::drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, const char* rightLabel) const {
|
||||||
int currentX = rect.x + LyraMetrics::values.contentSidePadding;
|
int currentX = rect.x + M().contentSidePadding;
|
||||||
int rightSpace = LyraMetrics::values.contentSidePadding;
|
int rightSpace = M().contentSidePadding;
|
||||||
if (rightLabel) {
|
if (rightLabel) {
|
||||||
auto truncatedRightLabel =
|
auto truncatedRightLabel =
|
||||||
renderer.truncatedText(SMALL_FONT_ID, rightLabel, maxListValueWidth, EpdFontFamily::REGULAR);
|
renderer.truncatedText(SMALL_FONT_ID, rightLabel, maxListValueWidth, EpdFontFamily::REGULAR);
|
||||||
int rightLabelWidth = renderer.getTextWidth(SMALL_FONT_ID, truncatedRightLabel.c_str());
|
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());
|
rect.y + 7, truncatedRightLabel.c_str());
|
||||||
rightSpace += rightLabelWidth + hPaddingInSelection;
|
rightSpace += rightLabelWidth + hPaddingInSelection;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto truncatedLabel = renderer.truncatedText(
|
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.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);
|
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,
|
void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs,
|
||||||
bool selected) const {
|
bool selected) const {
|
||||||
int currentX = rect.x + LyraMetrics::values.contentSidePadding;
|
const int pad = M().contentSidePadding;
|
||||||
|
const int spacing = M().tabSpacing;
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, Color::LightGray);
|
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;
|
int tabIndex = 0;
|
||||||
for (const auto& tab : tabs) {
|
for (const auto& tab : tabs) {
|
||||||
const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, tab.label, EpdFontFamily::REGULAR);
|
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(
|
if (currentX + slotW > rect.x && currentX < rect.x + rect.width) {
|
||||||
Rect{currentX, rect.y, textWidth + 2 * hPaddingInSelection + LyraMetrics::values.tabSpacing, rect.height},
|
TouchRegistry::getInstance().add(Rect{currentX, rect.y, slotW + spacing, rect.height}, idx, TouchRegistry::Tab);
|
||||||
tabIndex++, TouchRegistry::Tab);
|
|
||||||
|
|
||||||
if (tab.selected) {
|
if (tab.selected) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
renderer.fillRoundedRect(currentX, rect.y + 1, textWidth + 2 * hPaddingInSelection, rect.height - 4,
|
renderer.fillRoundedRect(currentX, rect.y + 1, slotW, rect.height - 4, cornerRadius, Color::Black);
|
||||||
cornerRadius, Color::Black);
|
} else {
|
||||||
} else {
|
renderer.fillRectDither(currentX, rect.y, slotW, rect.height - 3, Color::LightGray);
|
||||||
renderer.fillRectDither(currentX, rect.y, textWidth + 2 * hPaddingInSelection, rect.height - 3,
|
renderer.drawLine(currentX, rect.y + rect.height - 3, currentX + slotW, rect.y + rect.height - 3, 2, true);
|
||||||
Color::LightGray);
|
}
|
||||||
renderer.drawLine(currentX, rect.y + rect.height - 3, currentX + textWidth + 2 * hPaddingInSelection,
|
|
||||||
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),
|
currentX += slotW + spacing;
|
||||||
EpdFontFamily::REGULAR);
|
|
||||||
|
|
||||||
currentX += textWidth + LyraMetrics::values.tabSpacing + 2 * hPaddingInSelection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width - 1, rect.y + rect.height - 1, true);
|
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 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;
|
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<std::string(int index)>& rowValue, bool highlightValue,
|
||||||
const std::function<bool(int index)>& rowDimmed) const {
|
const std::function<bool(int index)>& rowDimmed) const {
|
||||||
int rowHeight =
|
int rowHeight =
|
||||||
(rowSubtitle != nullptr) ? LyraMetrics::values.listWithSubtitleRowHeight : LyraMetrics::values.listRowHeight;
|
(rowSubtitle != nullptr) ? M().listWithSubtitleRowHeight : M().listRowHeight;
|
||||||
int pageItems = rect.height / rowHeight;
|
int pageItems = rect.height / rowHeight;
|
||||||
|
|
||||||
const int totalPages = (itemCount + pageItems - 1) / pageItems;
|
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 scrollBarHeight = (scrollAreaHeight * pageItems) / itemCount;
|
||||||
const int currentPage = selectedIndex / pageItems;
|
const int currentPage = selectedIndex / pageItems;
|
||||||
const int scrollBarY = rect.y + ((scrollAreaHeight - scrollBarHeight) * currentPage) / (totalPages - 1);
|
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.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);
|
scrollBarHeight, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw selection
|
// Draw selection
|
||||||
int contentWidth =
|
int contentWidth =
|
||||||
rect.width -
|
rect.width -
|
||||||
(totalPages > 1 ? (LyraMetrics::values.scrollBarWidth + LyraMetrics::values.scrollBarRightOffset) : 1);
|
(totalPages > 1 ? (M().scrollBarWidth + M().scrollBarRightOffset) : 1);
|
||||||
if (selectedIndex >= 0) {
|
if (selectedIndex >= 0) {
|
||||||
renderer.fillRoundedRect(
|
renderer.fillRoundedRect(
|
||||||
rect.x + LyraMetrics::values.contentSidePadding, rect.y + selectedIndex % pageItems * rowHeight,
|
rect.x + M().contentSidePadding, rect.y + selectedIndex % pageItems * rowHeight,
|
||||||
contentWidth - LyraMetrics::values.contentSidePadding * 2, rowHeight, cornerRadius, Color::LightGray);
|
contentWidth - M().contentSidePadding * 2, rowHeight, cornerRadius, Color::LightGray);
|
||||||
}
|
}
|
||||||
|
|
||||||
int textX = rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection;
|
int textX = rect.x + M().contentSidePadding + hPaddingInSelection;
|
||||||
int textWidth = contentWidth - LyraMetrics::values.contentSidePadding * 2 - hPaddingInSelection * 2;
|
int textWidth = contentWidth - M().contentSidePadding * 2 - hPaddingInSelection * 2;
|
||||||
int iconSize;
|
int iconSize = 0;
|
||||||
if (rowIcon != nullptr) {
|
if (rowIcon != nullptr) {
|
||||||
iconSize = (rowSubtitle != nullptr) ? mainMenuIconSize : listIconSize;
|
iconSize = (rowSubtitle != nullptr) ? mainMenuIconSize : listIconSize;
|
||||||
textX += iconSize + hPaddingInSelection;
|
textX += iconSize + hPaddingInSelection;
|
||||||
textWidth -= 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
|
// Draw all items
|
||||||
const auto pageStartIndex = selectedIndex / pageItems * pageItems;
|
const auto pageStartIndex = selectedIndex / pageItems * pageItems;
|
||||||
int iconY = (rowSubtitle != nullptr) ? 16 : 10;
|
|
||||||
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) {
|
for (int i = pageStartIndex; i < itemCount && i < pageStartIndex + pageItems; i++) {
|
||||||
const int itemY = rect.y + (i % pageItems) * rowHeight;
|
const int itemY = rect.y + (i % pageItems) * rowHeight;
|
||||||
TouchRegistry::getInstance().add(Rect{rect.x, itemY, rect.width, rowHeight}, i, TouchRegistry::Item);
|
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;
|
rowTextWidth -= valueWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const int titleTop = itemY + titleOffset;
|
||||||
auto itemName = rowTitle(i);
|
auto itemName = rowTitle(i);
|
||||||
auto item = renderer.truncatedText(UI_10_FONT_ID, itemName.c_str(), rowTextWidth);
|
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
|
// Apply checkerboard dither to create gray text effect for dimmed items
|
||||||
if (rowDimmed && rowDimmed(i) && i != selectedIndex) {
|
if (rowDimmed && rowDimmed(i) && i != selectedIndex) {
|
||||||
const int titleWidth = renderer.getTextWidth(UI_10_FONT_ID, item.c_str());
|
const int titleWidth = renderer.getTextWidth(UI_10_FONT_ID, item.c_str());
|
||||||
const int lineH = renderer.getLineHeight(UI_10_FONT_ID);
|
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++)
|
for (int px = textX; px < textX + titleWidth; px++)
|
||||||
if ((px + py) % 2 == 0) renderer.drawPixel(px, py, false);
|
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);
|
UIIcon icon = rowIcon(i);
|
||||||
const uint8_t* iconBitmap = iconForName(icon, iconSize);
|
const uint8_t* iconBitmap = iconForName(icon, iconSize);
|
||||||
if (iconBitmap != nullptr) {
|
if (iconBitmap != nullptr) {
|
||||||
renderer.drawIcon(iconBitmap, rect.x + LyraMetrics::values.contentSidePadding + hPaddingInSelection,
|
const int iconDrawY = iconYForText(renderer, UI_10_FONT_ID, titleTop, iconSize);
|
||||||
itemY + iconY, iconSize, 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 (!valueText.empty()) {
|
||||||
if (i == selectedIndex && highlightValue) {
|
if (i == selectedIndex && highlightValue) {
|
||||||
renderer.fillRoundedRect(
|
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);
|
valueWidth + hPaddingInSelection, rowHeight, cornerRadius, Color::Black);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,7 +355,7 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
|||||||
if (rowSubtitle != nullptr) {
|
if (rowSubtitle != nullptr) {
|
||||||
valueY = itemY + 16;
|
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));
|
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,
|
void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
||||||
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
||||||
bool& bufferRestored, std::function<bool()> storeCoverBuffer) const {
|
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 tileHeight = rect.height;
|
||||||
const int tileY = rect.y;
|
const int tileY = rect.y;
|
||||||
const bool hasContinueReading = !recentBooks.empty();
|
const bool hasContinueReading = !recentBooks.empty();
|
||||||
if (coverWidth == 0) {
|
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
|
// 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.
|
// recentBooks[0] (home selector 0) — full card width, not just the cover photo.
|
||||||
if (hasContinueReading) {
|
if (hasContinueReading) {
|
||||||
TouchRegistry::getInstance().add(
|
TouchRegistry::getInstance().add(
|
||||||
Rect{LyraMetrics::values.contentSidePadding, tileY, tileWidth,
|
Rect{M().contentSidePadding, tileY, tileWidth,
|
||||||
LyraMetrics::values.homeCoverHeight + 2 * hPaddingInSelection},
|
M().homeCoverHeight + 2 * hPaddingInSelection},
|
||||||
0, TouchRegistry::Cover);
|
0, TouchRegistry::Cover);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,11 +477,11 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
if (!coverRendered) {
|
if (!coverRendered) {
|
||||||
std::string coverPath = book.coverBmpPath;
|
std::string coverPath = book.coverBmpPath;
|
||||||
bool hasCover = true;
|
bool hasCover = true;
|
||||||
int tileX = LyraMetrics::values.contentSidePadding;
|
int tileX = M().contentSidePadding;
|
||||||
if (coverPath.empty()) {
|
if (coverPath.empty()) {
|
||||||
hasCover = false;
|
hasCover = false;
|
||||||
} else {
|
} 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
|
// First time: load cover from SD and render
|
||||||
HalFile file;
|
HalFile file;
|
||||||
@@ -454,7 +490,7 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
coverWidth = bitmap.getWidth();
|
coverWidth = bitmap.getWidth();
|
||||||
renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth,
|
renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth,
|
||||||
LyraMetrics::values.homeCoverHeight);
|
M().homeCoverHeight);
|
||||||
} else {
|
} else {
|
||||||
hasCover = false;
|
hasCover = false;
|
||||||
}
|
}
|
||||||
@@ -464,13 +500,13 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std:
|
|||||||
|
|
||||||
// Draw either way
|
// Draw either way
|
||||||
renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth,
|
renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, coverWidth,
|
||||||
LyraMetrics::values.homeCoverHeight, true);
|
M().homeCoverHeight, true);
|
||||||
|
|
||||||
if (!hasCover) {
|
if (!hasCover) {
|
||||||
// Render empty cover
|
// Render empty cover
|
||||||
renderer.fillRect(tileX + hPaddingInSelection,
|
renderer.fillRect(tileX + hPaddingInSelection,
|
||||||
tileY + hPaddingInSelection + (LyraMetrics::values.homeCoverHeight / 3), coverWidth,
|
tileY + hPaddingInSelection + (M().homeCoverHeight / 3), coverWidth,
|
||||||
2 * LyraMetrics::values.homeCoverHeight / 3, true);
|
2 * M().homeCoverHeight / 3, true);
|
||||||
renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32, 32);
|
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);
|
bool bookSelected = (selectorIndex == 0);
|
||||||
|
|
||||||
int tileX = LyraMetrics::values.contentSidePadding;
|
int tileX = M().contentSidePadding;
|
||||||
int textWidth = tileWidth - 2 * hPaddingInSelection - LyraMetrics::values.verticalSpacing - coverWidth;
|
int textWidth = tileWidth - 2 * hPaddingInSelection - M().verticalSpacing - coverWidth;
|
||||||
|
|
||||||
if (bookSelected) {
|
if (bookSelected) {
|
||||||
// Draw selection box
|
// Draw selection box
|
||||||
renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false,
|
renderer.fillRoundedRect(tileX, tileY, tileWidth, hPaddingInSelection, cornerRadius, true, true, false, false,
|
||||||
Color::LightGray);
|
Color::LightGray);
|
||||||
renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection,
|
renderer.fillRectDither(tileX, tileY + hPaddingInSelection, hPaddingInSelection,
|
||||||
LyraMetrics::values.homeCoverHeight, Color::LightGray);
|
M().homeCoverHeight, Color::LightGray);
|
||||||
renderer.fillRectDither(tileX + hPaddingInSelection + coverWidth, tileY + hPaddingInSelection,
|
renderer.fillRectDither(tileX + hPaddingInSelection + coverWidth, tileY + hPaddingInSelection,
|
||||||
tileWidth - hPaddingInSelection - coverWidth, LyraMetrics::values.homeCoverHeight,
|
tileWidth - hPaddingInSelection - coverWidth, M().homeCoverHeight,
|
||||||
Color::LightGray);
|
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);
|
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 authorHeight = book.author.empty() ? 0 : (renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2);
|
||||||
const int totalBlockHeight = titleBlockHeight + authorHeight;
|
const int totalBlockHeight = titleBlockHeight + authorHeight;
|
||||||
int titleY = tileY + tileHeight / 2 - totalBlockHeight / 2;
|
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) {
|
for (const auto& line : titleLines) {
|
||||||
renderer.drawText(UI_12_FONT_ID, textX, titleY, line.c_str(), true, EpdFontFamily::BOLD);
|
renderer.drawText(UI_12_FONT_ID, textX, titleY, line.c_str(), true, EpdFontFamily::BOLD);
|
||||||
titleY += titleLineHeight;
|
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,
|
void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount, int selectedIndex,
|
||||||
const std::function<std::string(int index)>& buttonLabel,
|
const std::function<std::string(int index)>& buttonLabel,
|
||||||
const std::function<UIIcon(int index)>& rowIcon) const {
|
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) {
|
for (int i = 0; i < buttonCount; ++i) {
|
||||||
int tileWidth = rect.width - LyraMetrics::values.contentSidePadding * 2;
|
int tileWidth = rect.width - M().contentSidePadding * 2;
|
||||||
Rect tileRect = Rect{rect.x + LyraMetrics::values.contentSidePadding,
|
Rect tileRect = Rect{rect.x + M().contentSidePadding, rect.y + i * stride, tileWidth, rowHeight};
|
||||||
rect.y + i * (LyraMetrics::values.menuRowHeight + LyraMetrics::values.menuSpacing), tileWidth,
|
|
||||||
LyraMetrics::values.menuRowHeight};
|
|
||||||
TouchRegistry::getInstance().add(tileRect, i, TouchRegistry::Item);
|
TouchRegistry::getInstance().add(tileRect, i, TouchRegistry::Item);
|
||||||
|
|
||||||
const bool selected = selectedIndex == i;
|
const bool selected = selectedIndex == i;
|
||||||
@@ -546,13 +588,14 @@ void LyraTheme::drawButtonMenu(GfxRenderer& renderer, Rect rect, int buttonCount
|
|||||||
const char* label = labelStr.c_str();
|
const char* label = labelStr.c_str();
|
||||||
int textX = tileRect.x + 16;
|
int textX = tileRect.x + 16;
|
||||||
const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID);
|
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) {
|
if (rowIcon != nullptr) {
|
||||||
UIIcon icon = rowIcon(i);
|
UIIcon icon = rowIcon(i);
|
||||||
const uint8_t* iconBitmap = iconForName(icon, mainMenuIconSize);
|
const uint8_t* iconBitmap = iconForName(icon, mainMenuIconSize);
|
||||||
if (iconBitmap != nullptr) {
|
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;
|
textX += mainMenuIconSize + hPaddingInSelection + 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ constexpr ThemeMetrics values = {.batteryWidth = 16,
|
|||||||
.listRowHeight = 40,
|
.listRowHeight = 40,
|
||||||
.listWithSubtitleRowHeight = 60,
|
.listWithSubtitleRowHeight = 60,
|
||||||
.menuRowHeight = 64,
|
.menuRowHeight = 64,
|
||||||
.menuSpacing = 8,
|
.menuSpacing = 14,
|
||||||
.tabSpacing = 8,
|
.tabSpacing = 8,
|
||||||
.tabBarHeight = 40,
|
.tabBarHeight = 40,
|
||||||
.scrollBarWidth = 4,
|
.scrollBarWidth = 4,
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
namespace {
|
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 kCoverRadius = 18;
|
||||||
constexpr int kMenuRadius = 30;
|
constexpr int kMenuRadius = 30;
|
||||||
constexpr int kBottomRadius = 15;
|
constexpr int kBottomRadius = 15;
|
||||||
@@ -30,8 +35,8 @@ void drawScrollBar(const GfxRenderer& renderer, Rect rect, int itemCount, int pa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int barW = RoundedRaffMetrics::values.scrollBarWidth;
|
const int barW = M().scrollBarWidth;
|
||||||
const int barX = rect.x + rect.width - RoundedRaffMetrics::values.scrollBarRightOffset - barW;
|
const int barX = rect.x + rect.width - M().scrollBarRightOffset - barW;
|
||||||
const int barY = rect.y;
|
const int barY = rect.y;
|
||||||
const int barH = rect.height;
|
const int barH = rect.height;
|
||||||
|
|
||||||
@@ -54,13 +59,13 @@ void RoundedRaffTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const
|
|||||||
if (title == nullptr) {
|
if (title == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const int sidePadding = RoundedRaffMetrics::values.contentSidePadding;
|
const int sidePadding = M().contentSidePadding;
|
||||||
const int titleX = rect.x + sidePadding;
|
const int titleX = rect.x + sidePadding;
|
||||||
const int titleY = rect.y + 14;
|
const int titleY = rect.y + 14;
|
||||||
|
|
||||||
const bool showBatteryPercentage =
|
const bool showBatteryPercentage =
|
||||||
SETTINGS.hideBatteryPercentage != CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_ALWAYS;
|
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
|
// Reserve space for the widest possible percentage text to avoid title/battery overlap
|
||||||
int batteryGroupLeftX = batteryIconX;
|
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%");
|
const int maxTextWidth = renderer.getTextWidth(SMALL_FONT_ID, "100%");
|
||||||
batteryGroupLeftX -= maxTextWidth + batteryPercentSpacing;
|
batteryGroupLeftX -= maxTextWidth + batteryPercentSpacing;
|
||||||
|
|
||||||
const int clearW = maxTextWidth + batteryPercentSpacing + RoundedRaffMetrics::values.batteryWidth;
|
const int clearW = maxTextWidth + batteryPercentSpacing + M().batteryWidth;
|
||||||
const int clearH = std::max(renderer.getTextHeight(SMALL_FONT_ID), RoundedRaffMetrics::values.batteryHeight + 8);
|
const int clearH = std::max(renderer.getTextHeight(SMALL_FONT_ID), M().batteryHeight + 8);
|
||||||
renderer.fillRect(batteryIconX - maxTextWidth - batteryPercentSpacing, rect.y + 14, clearW, clearH, false);
|
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);
|
auto headerTitle = renderer.truncatedText(kTitleFontId, title, maxTitleWidth, EpdFontFamily::BOLD);
|
||||||
renderer.drawText(kTitleFontId, titleX, titleY, headerTitle.c_str(), true, EpdFontFamily::BOLD);
|
renderer.drawText(kTitleFontId, titleX, titleY, headerTitle.c_str(), true, EpdFontFamily::BOLD);
|
||||||
drawBatteryRight(renderer,
|
drawBatteryRight(renderer,
|
||||||
Rect{batteryIconX, rect.y + 14, RoundedRaffMetrics::values.batteryWidth,
|
Rect{batteryIconX, rect.y + 14, M().batteryWidth,
|
||||||
RoundedRaffMetrics::values.batteryHeight},
|
M().batteryHeight},
|
||||||
showBatteryPercentage);
|
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,
|
void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
||||||
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
||||||
bool& bufferRestored, std::function<bool()> storeCoverBuffer) const {
|
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 tileHeight = rect.height;
|
||||||
const int tileY = rect.y;
|
const int tileY = rect.y;
|
||||||
const bool hasContinueReading = !recentBooks.empty();
|
const bool hasContinueReading = !recentBooks.empty();
|
||||||
if (coverWidth == 0) {
|
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 imgY = tileY + (tileHeight - M().homeCoverHeight) / 2;
|
||||||
const int tileX = RoundedRaffMetrics::values.contentSidePadding;
|
const int tileX = M().contentSidePadding;
|
||||||
|
|
||||||
// Tapping the continue-reading cover opens recentBooks[0] (home selector 0).
|
// Tapping the continue-reading cover opens recentBooks[0] (home selector 0).
|
||||||
if (hasContinueReading) {
|
if (hasContinueReading) {
|
||||||
TouchRegistry::getInstance().add(
|
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);
|
TouchRegistry::Cover);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +153,7 @@ void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
|||||||
hasCover = false;
|
hasCover = false;
|
||||||
} else {
|
} else {
|
||||||
const std::string coverBmpPath =
|
const std::string coverBmpPath =
|
||||||
UITheme::getCoverThumbPath(coverPath, RoundedRaffMetrics::values.homeCoverHeight);
|
UITheme::getCoverThumbPath(coverPath, M().homeCoverHeight);
|
||||||
|
|
||||||
// First time: load cover from SD and render
|
// First time: load cover from SD and render
|
||||||
HalFile file;
|
HalFile file;
|
||||||
@@ -157,9 +162,9 @@ void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
|||||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||||
coverWidth = bitmap.getWidth();
|
coverWidth = bitmap.getWidth();
|
||||||
renderer.drawBitmap(bitmap, tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth,
|
renderer.drawBitmap(bitmap, tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth,
|
||||||
RoundedRaffMetrics::values.homeCoverHeight);
|
M().homeCoverHeight);
|
||||||
renderer.maskRoundedRectOutsideCorners(tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth,
|
renderer.maskRoundedRectOutsideCorners(tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth,
|
||||||
RoundedRaffMetrics::values.homeCoverHeight, kCoverRadius,
|
M().homeCoverHeight, kCoverRadius,
|
||||||
Color::LightGray);
|
Color::LightGray);
|
||||||
} else {
|
} else {
|
||||||
hasCover = false;
|
hasCover = false;
|
||||||
@@ -170,15 +175,15 @@ void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con
|
|||||||
|
|
||||||
// Draw either way
|
// Draw either way
|
||||||
renderer.drawRoundedRect(tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth,
|
renderer.drawRoundedRect(tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth,
|
||||||
RoundedRaffMetrics::values.homeCoverHeight, 1, kCoverRadius, true);
|
M().homeCoverHeight, 1, kCoverRadius, true);
|
||||||
|
|
||||||
if (!hasCover) {
|
if (!hasCover) {
|
||||||
// Render empty cover
|
// Render empty cover
|
||||||
renderer.fillRect(tileX + (tileWidth - coverWidth) / 2, imgY + (RoundedRaffMetrics::values.homeCoverHeight / 3),
|
renderer.fillRect(tileX + (tileWidth - coverWidth) / 2, imgY + (M().homeCoverHeight / 3),
|
||||||
coverWidth, 2 * RoundedRaffMetrics::values.homeCoverHeight / 3, true);
|
coverWidth, 2 * M().homeCoverHeight / 3, true);
|
||||||
renderer.drawIcon(CoverIcon, tileX + (tileWidth - coverWidth) / 2 + 24, imgY + 24, 32, 32);
|
renderer.drawIcon(CoverIcon, tileX + (tileWidth - coverWidth) / 2 + 24, imgY + 24, 32, 32);
|
||||||
renderer.maskRoundedRectOutsideCorners(tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth,
|
renderer.maskRoundedRectOutsideCorners(tileX + (tileWidth - coverWidth) / 2, imgY, coverWidth,
|
||||||
RoundedRaffMetrics::values.homeCoverHeight, kCoverRadius,
|
M().homeCoverHeight, kCoverRadius,
|
||||||
Color::LightGray);
|
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,
|
renderer.fillRoundedRect(tileX, tileY, tileWidth, imgY - tileY, kRowRadius, true, true, false, false,
|
||||||
Color::LightGray);
|
Color::LightGray);
|
||||||
renderer.fillRectDither(tileX, imgY, (tileWidth - coverWidth) / 2, RoundedRaffMetrics::values.homeCoverHeight,
|
renderer.fillRectDither(tileX, imgY, (tileWidth - coverWidth) / 2, M().homeCoverHeight,
|
||||||
Color::LightGray);
|
Color::LightGray);
|
||||||
renderer.fillRectDither(tileX + (tileWidth + coverWidth) / 2, imgY, (tileWidth - coverWidth) / 2,
|
renderer.fillRectDither(tileX + (tileWidth + coverWidth) / 2, imgY, (tileWidth - coverWidth) / 2,
|
||||||
RoundedRaffMetrics::values.homeCoverHeight, Color::LightGray);
|
M().homeCoverHeight, Color::LightGray);
|
||||||
renderer.fillRoundedRect(tileX, imgY + RoundedRaffMetrics::values.homeCoverHeight, tileWidth,
|
renderer.fillRoundedRect(tileX, imgY + M().homeCoverHeight, tileWidth,
|
||||||
tileHeight - (imgY - tileY + RoundedRaffMetrics::values.homeCoverHeight), kRowRadius,
|
tileHeight - (imgY - tileY + M().homeCoverHeight), kRowRadius,
|
||||||
false, false, true, true, Color::LightGray);
|
false, false, true, true, Color::LightGray);
|
||||||
} else {
|
} else {
|
||||||
renderer.fillRoundedRect(tileX, tileY, tileWidth, tileHeight, kRowRadius, Color::LightGray);
|
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<std::string(int index)>& buttonLabel,
|
||||||
const std::function<UIIcon(int index)>& rowIcon) const {
|
const std::function<UIIcon(int index)>& rowIcon) const {
|
||||||
(void)rowIcon;
|
(void)rowIcon;
|
||||||
const int sidePadding = RoundedRaffMetrics::values.contentSidePadding;
|
const int sidePadding = M().contentSidePadding;
|
||||||
const int rowX = rect.x + sidePadding;
|
const int rowX = rect.x + sidePadding;
|
||||||
const int rowHeight = renderer.getLineHeight(kTitleFontId) + 20; // 10px top + 10px bottom
|
const int rowHeight = renderer.getLineHeight(kTitleFontId) + 20; // 10px top + 10px bottom
|
||||||
const int rowGap = kSelectableRowGap;
|
const int rowGap = kSelectableRowGap;
|
||||||
@@ -330,12 +335,12 @@ void RoundedRaffTheme::drawList(const GfxRenderer& renderer, Rect rect, int item
|
|||||||
constexpr int subtitleInterLineGap = 4;
|
constexpr int subtitleInterLineGap = 4;
|
||||||
const int subtitleRowHeight =
|
const int subtitleRowHeight =
|
||||||
subtitleTopPadding + titleLineHeight + subtitleInterLineGap + subtitleLineHeight + subtitleBottomPadding;
|
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 rowStep = rowHeight + kSelectableRowGap;
|
||||||
const int pageItems = std::max(1, rect.height / rowStep);
|
const int pageItems = std::max(1, rect.height / rowStep);
|
||||||
const int pageStartIndex = std::max(0, selectedIndex / pageItems) * pageItems;
|
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 rowX = rect.x + sidePadding;
|
||||||
const int rowWidth = rect.width - sidePadding * 2;
|
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 sidePadding = 20;
|
||||||
const int groupGap = 10;
|
const int groupGap = 10;
|
||||||
const int bottomMargin = 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 groupWidth = (pageWidth - sidePadding * 2 - groupGap) / 2;
|
||||||
const int hintY = pageHeight - hintHeight - bottomMargin;
|
const int hintY = pageHeight - hintHeight - bottomMargin;
|
||||||
const int textY = hintY + (hintHeight - renderer.getLineHeight(kGuideFontId)) / 2;
|
const int textY = hintY + (hintHeight - renderer.getLineHeight(kGuideFontId)) / 2;
|
||||||
|
|||||||
+32
-2
@@ -1,5 +1,7 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <Epub.h>
|
#include <Epub.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
#include <FontCacheManager.h>
|
#include <FontCacheManager.h>
|
||||||
#include <FontDecompressor.h>
|
#include <FontDecompressor.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
@@ -297,6 +299,34 @@ void setupDisplayAndFonts(bool seamless = false) {
|
|||||||
renderer.insertFont(UI_12_FONT_ID, ui12FontFamily);
|
renderer.insertFont(UI_12_FONT_ID, ui12FontFamily);
|
||||||
renderer.insertFont(SMALL_FONT_ID, smallFontFamily);
|
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
|
// Discover and load SD card fonts
|
||||||
sdFontSystem.begin(renderer);
|
sdFontSystem.begin(renderer);
|
||||||
|
|
||||||
@@ -531,10 +561,10 @@ void loop() {
|
|||||||
|
|
||||||
// Check for any user activity (button press or release) or active background work
|
// Check for any user activity (button press or release) or active background work
|
||||||
static unsigned long lastActivityTime = millis();
|
static unsigned long lastActivityTime = millis();
|
||||||
if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || halTiltSensor.hadActivity() ||
|
if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || gpio.wasTouchActivity() || halTiltSensor.hadActivity() ||
|
||||||
activityManager.preventAutoSleep()) {
|
activityManager.preventAutoSleep()) {
|
||||||
lastActivityTime = millis(); // Reset inactivity timer
|
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;
|
static bool screenshotButtonsReleased = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user