feat: Add touch coordinate mapping and RTOS task yielding (#2481)
Co-authored-by: Julia Nguyen <julia@uxj.io>
This commit is contained in:
co-authored by
Julia Nguyen
parent
c9188a7347
commit
f42fab1c66
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include <I18n.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -8,6 +9,7 @@
|
||||
#include "GfxRenderer.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
class OptionPopup {
|
||||
public:
|
||||
@@ -20,6 +22,7 @@ class OptionPopup {
|
||||
}
|
||||
selectedIndex = currentIndex;
|
||||
onSelectCallback = std::move(onSelect);
|
||||
layoutValid = false;
|
||||
active = true;
|
||||
}
|
||||
|
||||
@@ -32,6 +35,7 @@ class OptionPopup {
|
||||
}
|
||||
selectedIndex = currentIndex;
|
||||
onSelectCallback = std::move(onSelect);
|
||||
layoutValid = false;
|
||||
active = true;
|
||||
}
|
||||
|
||||
@@ -41,6 +45,7 @@ class OptionPopup {
|
||||
ownedStrings = options;
|
||||
selectedIndex = currentIndex;
|
||||
onSelectCallback = std::move(onSelect);
|
||||
layoutValid = false;
|
||||
active = true;
|
||||
}
|
||||
|
||||
@@ -48,6 +53,39 @@ class OptionPopup {
|
||||
if (!active) return false;
|
||||
|
||||
const int count = static_cast<int>(ownedStrings.size());
|
||||
int tx = 0;
|
||||
int ty = 0;
|
||||
if (input.wasScreenTouchDown(tx, ty)) {
|
||||
const auto& hitLayout = getLayout(input.getRenderer());
|
||||
for (int i = 0; i < static_cast<int>(hitLayout.options.size()); i++) {
|
||||
if (contains(hitLayout.options[i], tx, ty)) {
|
||||
if (selectedIndex != i) {
|
||||
selectedIndex = i;
|
||||
requestUpdate();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (input.wasScreenTapped(tx, ty)) {
|
||||
const auto& hitLayout = getLayout(input.getRenderer());
|
||||
for (int i = 0; i < static_cast<int>(hitLayout.options.size()); i++) {
|
||||
if (contains(hitLayout.options[i], tx, ty)) {
|
||||
selectedIndex = i;
|
||||
active = false;
|
||||
if (onSelectCallback) onSelectCallback(selectedIndex);
|
||||
requestUpdate();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Taps on the dialog chrome (title, padding) keep the popup open; taps outside dismiss it
|
||||
if (contains(hitLayout.dialog, tx, ty)) return true;
|
||||
active = false;
|
||||
requestUpdate();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (input.wasPressed(MappedInputManager::Button::Up) || input.wasPressed(MappedInputManager::Button::Left)) {
|
||||
selectedIndex = (selectedIndex - 1 + count) % count;
|
||||
requestUpdate();
|
||||
@@ -87,9 +125,69 @@ class OptionPopup {
|
||||
bool isActive() const { return active; }
|
||||
|
||||
private:
|
||||
struct Layout {
|
||||
Rect dialog{0, 0, 0, 0};
|
||||
std::vector<Rect> options;
|
||||
};
|
||||
|
||||
// Text measurement is expensive and wasScreenTouchDown() is level-triggered, so the
|
||||
// layout is computed once per show() and cached rather than rebuilt every loop().
|
||||
const Layout& getLayout(const GfxRenderer& renderer) const {
|
||||
if (layoutValid) return layout;
|
||||
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
const int optionFontId = metrics.optionPopupUseSmallFont ? UI_10_FONT_ID : UI_12_FONT_ID;
|
||||
const EpdFontFamily::Style optionStyle =
|
||||
metrics.optionPopupOptionFontBold ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR;
|
||||
|
||||
const int itemSpacing = metrics.optionPopupItemSpacing;
|
||||
const int innerPadding = metrics.optionPopupInnerPadding;
|
||||
const int selectionHPadding = metrics.optionPopupSelectionHPadding;
|
||||
const int selectionVPadding = metrics.optionPopupSelectionVPadding;
|
||||
|
||||
const int optionLineHeight = renderer.getLineHeight(optionFontId);
|
||||
const int titleLineHeight = renderer.getLineHeight(UI_12_FONT_ID);
|
||||
const int rowHeight = optionLineHeight + selectionVPadding * 2;
|
||||
|
||||
int maxTextWidth = renderer.getTextWidth(UI_12_FONT_ID, title.c_str(), EpdFontFamily::BOLD);
|
||||
for (const auto& opt : ownedStrings) {
|
||||
const int width = renderer.getTextWidth(optionFontId, opt.c_str(), optionStyle);
|
||||
if (width > maxTextWidth) maxTextWidth = width;
|
||||
}
|
||||
|
||||
const int optionCount = static_cast<int>(ownedStrings.size());
|
||||
const int listHeight = rowHeight * optionCount + itemSpacing * (optionCount - 1);
|
||||
const int dialogW = std::min((maxTextWidth + innerPadding * 2 + selectionHPadding * 2) * 12 / 10,
|
||||
pageWidth - metrics.optionPopupDialogSideMargin * 2);
|
||||
const int contentHeight = titleLineHeight + metrics.optionPopupTitleGap + listHeight;
|
||||
const int dialogH = contentHeight + innerPadding * 2;
|
||||
const int dialogX = (pageWidth - dialogW) / 2;
|
||||
const int dialogY = (pageHeight - dialogH) / 2;
|
||||
const int itemRectX = dialogX + innerPadding;
|
||||
const int itemRectW = dialogW - innerPadding * 2;
|
||||
const int firstItemY = dialogY + innerPadding + titleLineHeight + metrics.optionPopupTitleGap;
|
||||
|
||||
layout.dialog = Rect{dialogX, dialogY, dialogW, dialogH};
|
||||
layout.options.clear();
|
||||
layout.options.reserve(optionCount);
|
||||
for (int i = 0; i < optionCount; i++) {
|
||||
layout.options.push_back(Rect{itemRectX, firstItemY + i * (rowHeight + itemSpacing), itemRectW, rowHeight});
|
||||
}
|
||||
layoutValid = true;
|
||||
return layout;
|
||||
}
|
||||
|
||||
static bool contains(const Rect& rect, const int x, const int y) {
|
||||
return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
|
||||
}
|
||||
|
||||
bool active = false;
|
||||
std::string title;
|
||||
std::vector<std::string> ownedStrings;
|
||||
int selectedIndex = 0;
|
||||
std::function<void(int)> onSelectCallback;
|
||||
mutable Layout layout;
|
||||
mutable bool layoutValid = false;
|
||||
};
|
||||
|
||||
+28
-11
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalGPIO.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <memory>
|
||||
@@ -48,11 +49,27 @@ void UITheme::setTheme(CrossPointSettings::UI_THEME type) {
|
||||
currentMetrics = &Lyra3CoversMetrics::values;
|
||||
break;
|
||||
}
|
||||
metricsValid = false;
|
||||
}
|
||||
|
||||
const ThemeMetrics& UITheme::getMetrics() const {
|
||||
// hasTouch() can flip once touch init completes after static construction, so the
|
||||
// cached copy is refreshed when the flag differs instead of copying the struct per call.
|
||||
const bool touch = gpio.hasTouch();
|
||||
if (!metricsValid || touch != metricsForTouch) {
|
||||
adjustedMetrics = *currentMetrics;
|
||||
if (touch) {
|
||||
adjustedMetrics.buttonHintsHeight = 0;
|
||||
}
|
||||
metricsForTouch = touch;
|
||||
metricsValid = true;
|
||||
}
|
||||
return adjustedMetrics;
|
||||
}
|
||||
|
||||
int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints,
|
||||
bool hasSubtitle, int extraReservedHeight) {
|
||||
const ThemeMetrics& metrics = UITheme::getInstance().getMetrics();
|
||||
const ThemeMetrics metrics = UITheme::getInstance().getMetrics();
|
||||
auto orientation = renderer.getOrientation();
|
||||
int reservedHeight = metrics.topPadding;
|
||||
if (hasHeader) {
|
||||
@@ -66,8 +83,7 @@ int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader
|
||||
reservedHeight += metrics.verticalSpacing + metrics.buttonHintsHeight;
|
||||
}
|
||||
const int availableHeight = renderer.getScreenHeight() - reservedHeight - extraReservedHeight;
|
||||
int rowHeight = hasSubtitle ? metrics.listWithSubtitleRowHeight : metrics.listRowHeight;
|
||||
return availableHeight / rowHeight;
|
||||
return UITheme::getInstance().getTheme().getListPageItems(availableHeight, hasSubtitle);
|
||||
}
|
||||
|
||||
// Screen area excluding the button hints
|
||||
@@ -76,27 +92,28 @@ Rect UITheme::getScreenSafeArea(const GfxRenderer& renderer, bool hasFrontButton
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
const int screenHeight = renderer.getScreenHeight();
|
||||
Rect safeArea = Rect{0, 0, screenWidth, screenHeight};
|
||||
const ThemeMetrics metrics = getMetrics();
|
||||
switch (orientation) {
|
||||
case GfxRenderer::Orientation::Portrait:
|
||||
if (hasFrontButtonHints) {
|
||||
safeArea.height -= currentMetrics->buttonHintsHeight;
|
||||
safeArea.height -= metrics.buttonHintsHeight;
|
||||
}
|
||||
break;
|
||||
case GfxRenderer::Orientation::LandscapeClockwise:
|
||||
if (hasFrontButtonHints) {
|
||||
safeArea.x += currentMetrics->buttonHintsHeight;
|
||||
safeArea.width -= currentMetrics->buttonHintsHeight;
|
||||
safeArea.x += metrics.buttonHintsHeight;
|
||||
safeArea.width -= metrics.buttonHintsHeight;
|
||||
}
|
||||
break;
|
||||
case GfxRenderer::Orientation::PortraitInverted:
|
||||
if (hasFrontButtonHints) {
|
||||
safeArea.y += currentMetrics->buttonHintsHeight;
|
||||
safeArea.height -= currentMetrics->buttonHintsHeight;
|
||||
safeArea.y += metrics.buttonHintsHeight;
|
||||
safeArea.height -= metrics.buttonHintsHeight;
|
||||
}
|
||||
break;
|
||||
case GfxRenderer::Orientation::LandscapeCounterClockwise:
|
||||
if (hasFrontButtonHints) {
|
||||
safeArea.width -= currentMetrics->buttonHintsHeight;
|
||||
safeArea.width -= metrics.buttonHintsHeight;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -128,7 +145,7 @@ UIIcon UITheme::getFileIcon(const std::string& filename) {
|
||||
}
|
||||
|
||||
int UITheme::getStatusBarHeight() {
|
||||
const ThemeMetrics& metrics = UITheme::getInstance().getMetrics();
|
||||
const ThemeMetrics metrics = UITheme::getInstance().getMetrics();
|
||||
|
||||
// Add status bar margin
|
||||
const bool showStatusBar =
|
||||
@@ -142,7 +159,7 @@ int UITheme::getStatusBarHeight() {
|
||||
}
|
||||
|
||||
int UITheme::getProgressBarHeight() {
|
||||
const ThemeMetrics& metrics = UITheme::getInstance().getMetrics();
|
||||
const ThemeMetrics metrics = UITheme::getInstance().getMetrics();
|
||||
const bool showProgressBar =
|
||||
SETTINGS.statusBarProgressBar != CrossPointSettings::STATUS_BAR_PROGRESS_BAR::HIDE_PROGRESS;
|
||||
return (showProgressBar ? (((SETTINGS.statusBarProgressBarThickness + 1) * 2) + metrics.progressBarMarginTop) : 0);
|
||||
|
||||
@@ -16,7 +16,7 @@ class UITheme {
|
||||
UITheme();
|
||||
static UITheme& getInstance() { return instance; }
|
||||
|
||||
const ThemeMetrics& getMetrics() const { return *currentMetrics; }
|
||||
const ThemeMetrics& getMetrics() const;
|
||||
const BaseTheme& getTheme() const { return *currentTheme; }
|
||||
Rect getScreenSafeArea(const GfxRenderer& renderer, bool hasFrontButtonHints = false,
|
||||
bool hasSideButtonHints = false);
|
||||
@@ -34,6 +34,9 @@ class UITheme {
|
||||
private:
|
||||
const ThemeMetrics* currentMetrics;
|
||||
std::unique_ptr<BaseTheme> currentTheme;
|
||||
mutable ThemeMetrics adjustedMetrics;
|
||||
mutable bool metricsValid = false;
|
||||
mutable bool metricsForTouch = false;
|
||||
};
|
||||
|
||||
// Helper macro to access current theme
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <Icon.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Generated by freeink-sdk/libs/assets/Icons/tools/gen_icons.py from Lucide search.svg.
|
||||
static const uint8_t Search24IconBits[] = {
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0xFF, 0xFC, 0x00, 0xFF, 0xF8, 0xFC, 0x7F, 0xF1, 0xFE, 0x3F,
|
||||
0xE3, 0xFF, 0x1F, 0xE7, 0xFF, 0x9F, 0xCF, 0xFF, 0xCF, 0xCF, 0xFF, 0xCF, 0xCF, 0xFF, 0xCF, 0xCF, 0xFF, 0xCF,
|
||||
0xCF, 0xFF, 0xCF, 0xCF, 0xFF, 0xCF, 0xE7, 0xFF, 0x9F, 0xE3, 0xFF, 0x1F, 0xF1, 0xFE, 0x3F, 0xF8, 0xFC, 0x1F,
|
||||
0xFC, 0x00, 0x8F, 0xFF, 0x03, 0xC7, 0xFF, 0xFF, 0xE3, 0xFF, 0xFF, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
static const freeink::Icon Search24Icon = {24, 24, 11, Search24IconBits};
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalClock.h>
|
||||
#include <HalGPIO.h>
|
||||
#include <HalPowerManager.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
@@ -156,6 +157,10 @@ void BaseTheme::drawProgressBar(const GfxRenderer& renderer, Rect rect, const si
|
||||
|
||||
void BaseTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3,
|
||||
const char* btn4) const {
|
||||
if (gpio.hasTouch()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const GfxRenderer::Orientation orig_orientation = renderer.getOrientation();
|
||||
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
||||
|
||||
@@ -186,6 +191,10 @@ void BaseTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c
|
||||
}
|
||||
|
||||
void BaseTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const {
|
||||
if (gpio.hasTouch()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
constexpr int buttonWidth = BaseMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated)
|
||||
constexpr int buttonHeight = 80; // Height on screen (width when rotated)
|
||||
@@ -250,9 +259,15 @@ void BaseTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* top
|
||||
}
|
||||
}
|
||||
|
||||
int BaseTheme::getListPageItems(int contentHeight, bool hasSubtitle) const {
|
||||
int BaseTheme::getListRowStep(bool hasSubtitle) const {
|
||||
int rowHeight = (hasSubtitle) ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight;
|
||||
return contentHeight / rowHeight;
|
||||
return rowHeight;
|
||||
}
|
||||
|
||||
int BaseTheme::getListPageItems(int contentHeight, bool hasSubtitle) const {
|
||||
const int rowStep = getListRowStep(hasSubtitle);
|
||||
if (rowStep <= 0) return 1;
|
||||
return std::max(1, contentHeight / rowStep);
|
||||
}
|
||||
|
||||
void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
||||
@@ -263,7 +278,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
const std::function<bool(int index)>& rowDimmed) const {
|
||||
int rowHeight =
|
||||
(rowSubtitle != nullptr) ? BaseMetrics::values.listWithSubtitleRowHeight : BaseMetrics::values.listRowHeight;
|
||||
int pageItems = rect.height / rowHeight;
|
||||
int pageItems = rowHeight > 0 ? std::max(1, rect.height / rowHeight) : 1;
|
||||
|
||||
const int totalPages = (itemCount + pageItems - 1) / pageItems;
|
||||
if (totalPages > 1) {
|
||||
@@ -432,6 +447,29 @@ void BaseTheme::drawTabBar(const GfxRenderer& renderer, const Rect rect, const s
|
||||
}
|
||||
}
|
||||
|
||||
bool BaseTheme::tabIndexFromPoint(const GfxRenderer& renderer, const Rect rect, const std::vector<TabInfo>& tabs,
|
||||
const int x, const int y, int& index) const {
|
||||
if (tabs.empty() || y < rect.y || y >= rect.y + rect.height) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int currentX = rect.x + BaseMetrics::values.contentSidePadding;
|
||||
for (size_t i = 0; i < tabs.size(); i++) {
|
||||
const auto& tab = tabs[i];
|
||||
const int textWidth =
|
||||
renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR);
|
||||
const int left = (i == 0) ? rect.x : currentX - BaseMetrics::values.tabSpacing / 2;
|
||||
const int right = currentX + textWidth + BaseMetrics::values.tabSpacing / 2;
|
||||
if (x >= left && x < right) {
|
||||
index = static_cast<int>(i);
|
||||
return true;
|
||||
}
|
||||
currentX += textWidth + BaseMetrics::values.tabSpacing;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Draw the "Recent Book" cover card on the home screen
|
||||
// TODO: Refactor method to make it cleaner, split into smaller methods
|
||||
void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
||||
@@ -912,104 +950,6 @@ void BaseTheme::drawTextField(const GfxRenderer& renderer, Rect rect, const int
|
||||
}
|
||||
}
|
||||
|
||||
void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected,
|
||||
const char* secondaryLabel, const KeyboardKeyType keyType,
|
||||
const bool inactiveSelection) const {
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
const int cr = metrics.keyboardKeyCornerRadius;
|
||||
const bool isSpecialKey = keyType == KeyboardKeyType::Shift || keyType == KeyboardKeyType::Mode ||
|
||||
keyType == KeyboardKeyType::Del || keyType == KeyboardKeyType::Space ||
|
||||
keyType == KeyboardKeyType::Ok || keyType == KeyboardKeyType::Disabled;
|
||||
|
||||
if (isSelected) {
|
||||
if (inactiveSelection) {
|
||||
if (cr > 0) {
|
||||
renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cr, Color::LightGray);
|
||||
} else {
|
||||
renderer.drawRect(rect.x, rect.y, rect.width, rect.height, 2, true);
|
||||
}
|
||||
} else if (keyType == KeyboardKeyType::Disabled) {
|
||||
if (cr > 0) {
|
||||
renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cr, Color::LightGray);
|
||||
} else {
|
||||
renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, Color::LightGray);
|
||||
}
|
||||
} else {
|
||||
if (cr > 0) {
|
||||
renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cr, Color::Black);
|
||||
} else {
|
||||
renderer.fillRect(rect.x, rect.y, rect.width, rect.height, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (metrics.keyboardFillUnselected) {
|
||||
if (keyType == KeyboardKeyType::Disabled) {
|
||||
if (cr > 0) {
|
||||
renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cr, Color::LightGray);
|
||||
} else {
|
||||
renderer.fillRectDither(rect.x, rect.y, rect.width, rect.height, Color::LightGray);
|
||||
}
|
||||
} else {
|
||||
if (cr > 0) {
|
||||
renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, cr, Color::White);
|
||||
} else {
|
||||
renderer.fillRect(rect.x, rect.y, rect.width, rect.height, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bool shouldDrawOutline =
|
||||
(metrics.keyboardDrawSpecialOutlineWhenUnselected && isSpecialKey) || metrics.keyboardOutlineAllUnselected;
|
||||
if (shouldDrawOutline) {
|
||||
if (cr > 0) {
|
||||
renderer.drawRoundedRect(rect.x, rect.y, rect.width, rect.height, 1, cr, true);
|
||||
} else {
|
||||
renderer.drawRect(rect.x, rect.y, rect.width, rect.height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bool invert = isSelected && !inactiveSelection;
|
||||
|
||||
if (keyType == KeyboardKeyType::Space) {
|
||||
const int lineHalfWidth = rect.width * 3 / 10;
|
||||
const int centerX = rect.x + rect.width / 2;
|
||||
const int lineY = rect.y + rect.height / 2 + 3;
|
||||
renderer.drawLine(centerX - lineHalfWidth, lineY, centerX + lineHalfWidth, lineY, 3, !invert);
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyType == KeyboardKeyType::Del) {
|
||||
const int centerX = rect.x + rect.width / 2;
|
||||
const int centerY = rect.y + rect.height / 2;
|
||||
const int arrowLen = rect.width / 4;
|
||||
const int arrowHead = std::max(metrics.keyboardMinArrowHeadSize, arrowLen / 2);
|
||||
renderer.drawLine(centerX - arrowLen / 2, centerY, centerX + arrowLen / 2, centerY, 3, !invert);
|
||||
renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY - arrowHead, 3,
|
||||
!invert);
|
||||
renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY + arrowHead, 3,
|
||||
!invert);
|
||||
return;
|
||||
}
|
||||
|
||||
if (label == nullptr || label[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool hasSecondary = secondaryLabel != nullptr && secondaryLabel[0] != '\0';
|
||||
const int itemWidth = renderer.getTextWidth(UI_12_FONT_ID, label);
|
||||
const int textX = rect.x + (rect.width - itemWidth) / 2;
|
||||
const int textY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2;
|
||||
|
||||
renderer.drawText(UI_12_FONT_ID, textX, textY, label, !invert);
|
||||
|
||||
if (hasSecondary) {
|
||||
const int secWidth = renderer.getTextWidth(SMALL_FONT_ID, secondaryLabel);
|
||||
renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - secWidth - metrics.keyboardSecondaryLabelRightPadding,
|
||||
rect.y + metrics.keyboardSecondaryLabelTopPadding, secondaryLabel, !invert);
|
||||
}
|
||||
}
|
||||
|
||||
void BaseTheme::drawOptionPopup(const GfxRenderer& renderer, const char* title, const std::vector<std::string>& options,
|
||||
int selectedIndex) const {
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
|
||||
@@ -61,24 +61,12 @@ struct ThemeMetrics {
|
||||
int progressBarMarginTop;
|
||||
int statusBarHorizontalMargin;
|
||||
int statusBarVerticalMargin;
|
||||
|
||||
int keyboardKeyWidth;
|
||||
int keyboardKeyHeight;
|
||||
int keyboardKeySpacing;
|
||||
int keyboardBottomKeyHeight;
|
||||
int keyboardBottomKeySpacing;
|
||||
bool keyboardBottomAligned;
|
||||
bool keyboardCenteredText;
|
||||
int keyboardVerticalOffset;
|
||||
int keyboardTextFieldWidthPercent;
|
||||
int keyboardWidthPercent;
|
||||
int keyboardKeyCornerRadius;
|
||||
bool keyboardFillUnselected;
|
||||
bool keyboardOutlineAllUnselected;
|
||||
bool keyboardDrawSpecialOutlineWhenUnselected;
|
||||
int keyboardSecondaryLabelRightPadding;
|
||||
int keyboardSecondaryLabelTopPadding;
|
||||
int keyboardMinArrowHeadSize;
|
||||
|
||||
float popupTopOffsetRatio;
|
||||
int popupMarginX;
|
||||
@@ -115,8 +103,6 @@ struct ThemeMetrics {
|
||||
|
||||
enum UIIcon { None = 0, Folder, Text, Image, Book, File, Recent, Settings, Transfer, Library, Wifi, Hotspot, Bookmark };
|
||||
|
||||
enum class KeyboardKeyType { Normal, Shift, Mode, Space, Del, Ok, Disabled };
|
||||
|
||||
// Default theme implementation (Classic Theme)
|
||||
// Additional themes can inherit from this and override methods as needed
|
||||
|
||||
@@ -150,23 +136,12 @@ constexpr ThemeMetrics values = {.batteryWidth = 15,
|
||||
.progressBarMarginTop = 1,
|
||||
.statusBarHorizontalMargin = 5,
|
||||
.statusBarVerticalMargin = 19,
|
||||
.keyboardKeyWidth = 22,
|
||||
.keyboardKeyHeight = 40,
|
||||
.keyboardKeyHeight = 48,
|
||||
.keyboardKeySpacing = 0,
|
||||
.keyboardBottomKeyHeight = 35,
|
||||
.keyboardBottomKeySpacing = 5,
|
||||
.keyboardBottomAligned = true,
|
||||
.keyboardCenteredText = false,
|
||||
.keyboardVerticalOffset = -13,
|
||||
.keyboardTextFieldWidthPercent = 85,
|
||||
.keyboardWidthPercent = 90,
|
||||
.keyboardKeyCornerRadius = 0,
|
||||
.keyboardFillUnselected = false,
|
||||
.keyboardOutlineAllUnselected = false,
|
||||
.keyboardDrawSpecialOutlineWhenUnselected = true,
|
||||
.keyboardSecondaryLabelRightPadding = 1,
|
||||
.keyboardSecondaryLabelTopPadding = 0,
|
||||
.keyboardMinArrowHeadSize = 0,
|
||||
.keyboardWidthPercent = 94,
|
||||
.popupTopOffsetRatio = 0.075f,
|
||||
.popupMarginX = 15,
|
||||
.popupMarginY = 15,
|
||||
@@ -212,6 +187,7 @@ class BaseTheme {
|
||||
virtual void drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3,
|
||||
const char* btn4) const;
|
||||
virtual void drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const;
|
||||
virtual int getListRowStep(bool hasSubtitle) const;
|
||||
virtual int getListPageItems(int contentHeight, bool hasSubtitle) const;
|
||||
virtual void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
||||
const std::function<std::string(int index)>& rowTitle,
|
||||
@@ -225,6 +201,8 @@ class BaseTheme {
|
||||
const char* rightLabel = nullptr) const;
|
||||
virtual void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs,
|
||||
bool selected) const;
|
||||
virtual bool tabIndexFromPoint(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs, int x, int y,
|
||||
int& index) const;
|
||||
virtual void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
||||
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
||||
bool& bufferRestored, std::function<bool()> storeCoverBuffer) const;
|
||||
@@ -242,9 +220,6 @@ class BaseTheme {
|
||||
void drawHelpText(const GfxRenderer& renderer, Rect rect, const char* label) const;
|
||||
virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth, bool cursorMode = false,
|
||||
int contentStartX = 0, int contentWidth = 0) const;
|
||||
virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected,
|
||||
const char* secondaryLabel = nullptr, KeyboardKeyType keyType = KeyboardKeyType::Normal,
|
||||
bool inactiveSelection = false) const;
|
||||
virtual bool showsFileIcons() const { return false; }
|
||||
|
||||
// Shared constants and helpers for battery drawing (used by all themes)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -205,9 +206,37 @@ void LyraTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const std::ve
|
||||
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 {
|
||||
bool LyraTheme::tabIndexFromPoint(const GfxRenderer& renderer, const Rect rect, const std::vector<TabInfo>& tabs,
|
||||
const int x, const int y, int& index) const {
|
||||
if (tabs.empty() || y < rect.y || y >= rect.y + rect.height) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int currentX = rect.x + LyraMetrics::values.contentSidePadding;
|
||||
for (size_t i = 0; i < tabs.size(); i++) {
|
||||
const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, tabs[i].label, EpdFontFamily::REGULAR);
|
||||
const int tabWidth = textWidth + 2 * hPaddingInSelection;
|
||||
const int left = (i == 0) ? rect.x : currentX - LyraMetrics::values.tabSpacing / 2;
|
||||
const int right = currentX + tabWidth + LyraMetrics::values.tabSpacing / 2;
|
||||
if (x >= left && x < right) {
|
||||
index = static_cast<int>(i);
|
||||
return true;
|
||||
}
|
||||
currentX += tabWidth + LyraMetrics::values.tabSpacing;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int LyraTheme::getListRowStep(bool hasSubtitle) const {
|
||||
int rowHeight = (hasSubtitle) ? LyraMetrics::values.listWithSubtitleRowHeight : LyraMetrics::values.listRowHeight;
|
||||
return contentHeight / rowHeight;
|
||||
return rowHeight;
|
||||
}
|
||||
|
||||
int LyraTheme::getListPageItems(int contentHeight, bool hasSubtitle) const {
|
||||
const int rowStep = getListRowStep(hasSubtitle);
|
||||
if (rowStep <= 0) return 1;
|
||||
return std::max(1, contentHeight / rowStep);
|
||||
}
|
||||
|
||||
void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
||||
@@ -218,7 +247,7 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
const std::function<bool(int index)>& rowDimmed) const {
|
||||
int rowHeight =
|
||||
(rowSubtitle != nullptr) ? LyraMetrics::values.listWithSubtitleRowHeight : LyraMetrics::values.listRowHeight;
|
||||
int pageItems = rect.height / rowHeight;
|
||||
int pageItems = rowHeight > 0 ? std::max(1, rect.height / rowHeight) : 1;
|
||||
|
||||
const int totalPages = (itemCount + pageItems - 1) / pageItems;
|
||||
if (totalPages > 1) {
|
||||
@@ -319,6 +348,10 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount,
|
||||
|
||||
void LyraTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3,
|
||||
const char* btn4) const {
|
||||
if (gpio.hasTouch()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const GfxRenderer::Orientation orig_orientation = renderer.getOrientation();
|
||||
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
||||
|
||||
@@ -357,6 +390,10 @@ void LyraTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const c
|
||||
}
|
||||
|
||||
void LyraTheme::drawSideButtonHints(const GfxRenderer& renderer, const char* topBtn, const char* bottomBtn) const {
|
||||
if (gpio.hasTouch()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
constexpr int buttonWidth = LyraMetrics::values.sideButtonHintsWidth; // Width on screen (height when rotated)
|
||||
constexpr int buttonHeight = 78; // Height on screen (width when rotated)
|
||||
|
||||
@@ -35,23 +35,12 @@ constexpr ThemeMetrics values = {.batteryWidth = 16,
|
||||
.progressBarMarginTop = 1,
|
||||
.statusBarHorizontalMargin = 5,
|
||||
.statusBarVerticalMargin = 19,
|
||||
.keyboardKeyWidth = 31,
|
||||
.keyboardKeyHeight = 40,
|
||||
.keyboardKeyHeight = 48,
|
||||
.keyboardKeySpacing = 0,
|
||||
.keyboardBottomKeyHeight = 35,
|
||||
.keyboardBottomKeySpacing = 5,
|
||||
.keyboardBottomAligned = true,
|
||||
.keyboardCenteredText = false,
|
||||
.keyboardVerticalOffset = -7,
|
||||
.keyboardTextFieldWidthPercent = 85,
|
||||
.keyboardWidthPercent = 90,
|
||||
.keyboardKeyCornerRadius = 6,
|
||||
.keyboardFillUnselected = false,
|
||||
.keyboardOutlineAllUnselected = false,
|
||||
.keyboardDrawSpecialOutlineWhenUnselected = true,
|
||||
.keyboardSecondaryLabelRightPadding = 1,
|
||||
.keyboardSecondaryLabelTopPadding = 0,
|
||||
.keyboardMinArrowHeadSize = 0,
|
||||
.keyboardWidthPercent = 94,
|
||||
.popupTopOffsetRatio = 0.165f,
|
||||
.popupMarginX = 16,
|
||||
.popupMarginY = 12,
|
||||
@@ -92,6 +81,9 @@ class LyraTheme : public BaseTheme {
|
||||
const char* rightLabel = nullptr) const override;
|
||||
void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs,
|
||||
bool selected) const override;
|
||||
bool tabIndexFromPoint(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs, int x, int y,
|
||||
int& index) const override;
|
||||
int getListRowStep(bool hasSubtitle) const override;
|
||||
int getListPageItems(int contentHeight, bool hasSubtitle) const override;
|
||||
void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
||||
const std::function<std::string(int index)>& rowTitle,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "RoundedRaffTheme.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalGPIO.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
|
||||
@@ -112,6 +113,18 @@ void RoundedRaffTheme::drawTabBar(const GfxRenderer& renderer, Rect rect, const
|
||||
renderer.drawLine(rect.x, rect.y + rect.height - 1, rect.x + rect.width - 1, rect.y + rect.height - 1, true);
|
||||
}
|
||||
|
||||
bool RoundedRaffTheme::tabIndexFromPoint(const GfxRenderer& renderer, const Rect rect, const std::vector<TabInfo>& tabs,
|
||||
const int x, const int y, int& index) const {
|
||||
(void)renderer;
|
||||
if (tabs.empty() || y < rect.y || y >= rect.y + rect.height || x < rect.x || x >= rect.x + rect.width) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const int slotWidth = std::max(1, rect.width / static_cast<int>(tabs.size()));
|
||||
index = std::min(static_cast<int>(tabs.size()) - 1, (x - rect.x) / slotWidth);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RoundedRaffTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
||||
const int selectorIndex, bool& coverRendered, bool& coverBufferStored,
|
||||
bool& bufferRestored, std::function<bool()> storeCoverBuffer) const {
|
||||
@@ -248,57 +261,14 @@ void RoundedRaffTheme::drawTextField(const GfxRenderer& renderer, Rect rect, con
|
||||
renderer.drawLine(lineStart, lineY, lineStart + lineW - 1, lineY, thickness, true);
|
||||
}
|
||||
|
||||
void RoundedRaffTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected,
|
||||
const char* secondaryLabel, const KeyboardKeyType keyType,
|
||||
const bool inactiveSelection) const {
|
||||
constexpr int keyRadius = 10;
|
||||
const bool disabled = keyType == KeyboardKeyType::Disabled;
|
||||
const bool invert = isSelected && !inactiveSelection;
|
||||
int RoundedRaffTheme::getListRowStep(bool hasSubtitle) const {
|
||||
const int rowHeight =
|
||||
hasSubtitle ? RoundedRaffMetrics::values.listWithSubtitleRowHeight : RoundedRaffMetrics::values.listRowHeight;
|
||||
return rowHeight + kSelectableRowGap;
|
||||
}
|
||||
|
||||
if (isSelected) {
|
||||
const Color fillColor = (inactiveSelection || disabled) ? Color::LightGray : Color::Black;
|
||||
renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, keyRadius, fillColor);
|
||||
} else {
|
||||
if (disabled) {
|
||||
renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, keyRadius, Color::LightGray);
|
||||
} else {
|
||||
renderer.fillRoundedRect(rect.x, rect.y, rect.width, rect.height, keyRadius, Color::White);
|
||||
}
|
||||
renderer.drawRoundedRect(rect.x, rect.y, rect.width, rect.height, 1, keyRadius, true);
|
||||
}
|
||||
|
||||
if (keyType == KeyboardKeyType::Space) {
|
||||
const int lineHalfWidth = rect.width * 3 / 10;
|
||||
const int centerX = rect.x + rect.width / 2;
|
||||
const int lineY = rect.y + rect.height / 2 + 3;
|
||||
renderer.drawLine(centerX - lineHalfWidth, lineY, centerX + lineHalfWidth, lineY, 3, !invert);
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyType == KeyboardKeyType::Del) {
|
||||
const int centerX = rect.x + rect.width / 2;
|
||||
const int centerY = rect.y + rect.height / 2;
|
||||
const int arrowLen = rect.width / 4;
|
||||
const int arrowHead = std::max(1, arrowLen / 2);
|
||||
renderer.drawLine(centerX - arrowLen / 2, centerY, centerX + arrowLen / 2, centerY, 3, !invert);
|
||||
renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY - arrowHead, 3,
|
||||
!invert);
|
||||
renderer.drawLine(centerX - arrowLen / 2, centerY, centerX - arrowLen / 2 + arrowHead, centerY + arrowHead, 3,
|
||||
!invert);
|
||||
return;
|
||||
}
|
||||
|
||||
if (label != nullptr && label[0] != '\0') {
|
||||
const int itemWidth = renderer.getTextWidth(UI_12_FONT_ID, label);
|
||||
const int textX = rect.x + (rect.width - itemWidth) / 2;
|
||||
const int textY = rect.y + (rect.height - renderer.getLineHeight(UI_12_FONT_ID)) / 2;
|
||||
renderer.drawText(UI_12_FONT_ID, textX, textY, label, !invert);
|
||||
}
|
||||
|
||||
if (secondaryLabel != nullptr && secondaryLabel[0] != '\0') {
|
||||
const int secWidth = renderer.getTextWidth(SMALL_FONT_ID, secondaryLabel);
|
||||
renderer.drawText(SMALL_FONT_ID, rect.x + rect.width - secWidth - 3, rect.y + 1, secondaryLabel, !invert);
|
||||
}
|
||||
int RoundedRaffTheme::getListPageItems(int contentHeight, bool hasSubtitle) const {
|
||||
return std::max(1, contentHeight / getListRowStep(hasSubtitle));
|
||||
}
|
||||
|
||||
void RoundedRaffTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
||||
@@ -383,6 +353,10 @@ void RoundedRaffTheme::drawList(const GfxRenderer& renderer, Rect rect, int item
|
||||
|
||||
void RoundedRaffTheme::drawButtonHints(GfxRenderer& renderer, const char* btn1, const char* btn2, const char* btn3,
|
||||
const char* btn4) const {
|
||||
if (gpio.hasTouch()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const GfxRenderer::Orientation origOrientation = renderer.getOrientation();
|
||||
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
|
||||
|
||||
|
||||
@@ -35,23 +35,12 @@ constexpr ThemeMetrics values = {.batteryWidth = 15,
|
||||
.progressBarMarginTop = 1,
|
||||
.statusBarHorizontalMargin = 5,
|
||||
.statusBarVerticalMargin = 19,
|
||||
.keyboardKeyWidth = 22,
|
||||
.keyboardKeyHeight = 30,
|
||||
.keyboardKeyHeight = 36,
|
||||
.keyboardKeySpacing = 10,
|
||||
.keyboardBottomKeyHeight = 30,
|
||||
.keyboardBottomKeySpacing = 5,
|
||||
.keyboardBottomAligned = true,
|
||||
.keyboardCenteredText = false,
|
||||
.keyboardVerticalOffset = 0,
|
||||
.keyboardTextFieldWidthPercent = 85,
|
||||
.keyboardWidthPercent = 90,
|
||||
.keyboardKeyCornerRadius = 10,
|
||||
.keyboardFillUnselected = true,
|
||||
.keyboardOutlineAllUnselected = true,
|
||||
.keyboardDrawSpecialOutlineWhenUnselected = true,
|
||||
.keyboardSecondaryLabelRightPadding = 3,
|
||||
.keyboardSecondaryLabelTopPadding = 1,
|
||||
.keyboardMinArrowHeadSize = 1,
|
||||
.keyboardWidthPercent = 94,
|
||||
.popupTopOffsetRatio = 0.12f,
|
||||
.popupMarginX = 20,
|
||||
.popupMarginY = 14,
|
||||
@@ -89,6 +78,8 @@ class RoundedRaffTheme : public BaseTheme {
|
||||
const char* subtitle = nullptr) const override;
|
||||
void drawTabBar(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs,
|
||||
bool selected) const override;
|
||||
bool tabIndexFromPoint(const GfxRenderer& renderer, Rect rect, const std::vector<TabInfo>& tabs, int x, int y,
|
||||
int& index) const override;
|
||||
void drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std::vector<RecentBook>& recentBooks,
|
||||
int selectorIndex, bool& coverRendered, bool& coverBufferStored, bool& bufferRestored,
|
||||
std::function<bool()> storeCoverBuffer) const override;
|
||||
@@ -97,9 +88,8 @@ class RoundedRaffTheme : public BaseTheme {
|
||||
const std::function<UIIcon(int index)>& rowIcon) const override;
|
||||
void drawTextField(const GfxRenderer& renderer, Rect rect, int textWidth, bool cursorMode = false,
|
||||
int contentStartX = 0, int contentWidth = 0) const override;
|
||||
void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, bool isSelected,
|
||||
const char* secondaryLabel = nullptr, KeyboardKeyType keyType = KeyboardKeyType::Normal,
|
||||
bool inactiveSelection = false) const override;
|
||||
int getListRowStep(bool hasSubtitle) const override;
|
||||
int getListPageItems(int contentHeight, bool hasSubtitle) const override;
|
||||
void drawList(const GfxRenderer& renderer, Rect rect, int itemCount, int selectedIndex,
|
||||
const std::function<std::string(int index)>& rowTitle,
|
||||
const std::function<std::string(int index)>& rowSubtitle = nullptr,
|
||||
|
||||
Reference in New Issue
Block a user