feat: Add touch coordinate mapping and RTOS task yielding (#2481)

Co-authored-by: Julia Nguyen <julia@uxj.io>
This commit is contained in:
Justin Mitchell
2026-07-20 16:31:07 -04:00
committed by GitHub
co-authored by Julia Nguyen
parent c9188a7347
commit f42fab1c66
123 changed files with 3564 additions and 1380 deletions
+98
View File
@@ -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
View File
@@ -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);
+4 -1
View File
@@ -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
+13
View File
@@ -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};
+41 -101
View File
@@ -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();
+5 -30
View File
@@ -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)
+40 -3
View File
@@ -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)
+5 -13
View File
@@ -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,