From c4424dfe1c51e05dc0efa385454be136111355b8 Mon Sep 17 00:00:00 2001 From: jpirnay Date: Thu, 9 Apr 2026 16:49:28 +0200 Subject: [PATCH] Introduce separators in drawList --- src/components/UITheme.cpp | 17 +++++ src/components/UITheme.h | 7 ++ src/components/themes/BaseTheme.cpp | 31 ++++++++- src/components/themes/BaseTheme.h | 2 + src/components/themes/lyra/LyraTheme.cpp | 20 +++++- src/util/ButtonNavigator.cpp | 82 ++++++++++++++++++++++++ src/util/ButtonNavigator.h | 13 ++++ 7 files changed, 169 insertions(+), 3 deletions(-) diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 4a61f00d..7b76f617 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -97,6 +98,22 @@ Rect UITheme::getContentRect(const GfxRenderer& renderer, bool hasBottomHints, b return Rect{left, top, w - left - right, h - top - bottom}; } +std::string UITheme::makeSeparatorTitle(const std::string& title) { + return std::string("__") + title; +} + +std::string UITheme::makeSeparatorTitle(StrId labelId) { + return std::string("__") + I18N.get(labelId); +} + +bool UITheme::isSeparatorTitle(const std::string& title) { + return title.rfind("__", 0) == 0; +} + +std::string UITheme::stripSeparatorTitle(const std::string& title) { + return isSeparatorTitle(title) ? title.substr(2) : title; +} + std::string UITheme::getCoverThumbPath(std::string coverBmpPath, int coverHeight) { size_t pos = coverBmpPath.find("[HEIGHT]", 0); if (pos != std::string::npos) { diff --git a/src/components/UITheme.h b/src/components/UITheme.h index e45c743d..17d5c997 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -3,6 +3,8 @@ #include #include +#include + #include "CrossPointSettings.h" #include "components/themes/BaseTheme.h" @@ -20,6 +22,11 @@ class UITheme { void setTheme(CrossPointSettings::UI_THEME type); static int getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader, bool hasTabBar, bool hasButtonHints, bool hasSubtitle); + static std::string makeSeparatorTitle(const std::string& title); + static std::string makeSeparatorTitle(StrId labelId); + static bool isSeparatorTitle(const std::string& title); + static std::string stripSeparatorTitle(const std::string& title); + // Returns the drawable content Rect accounting for screen orientation and visible button hints. // Bottom hints occupy the physical bottom edge; side hints occupy the physical right edge. // The mapping to logical edges is orientation-dependent. diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index d183a20d..e38a63e2 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -270,9 +270,14 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, } } + bool selectedIsSeparator = false; + if (selectedIndex >= 0 && selectedIndex < itemCount) { + selectedIsSeparator = UITheme::isSeparatorTitle(rowTitle(selectedIndex)); + } + // Draw selection int contentWidth = rect.width - 5; - if (selectedIndex >= 0) { + if (selectedIndex >= 0 && !selectedIsSeparator) { renderer.fillRect(rect.x, rect.y + selectedIndex % pageItems * rowHeight - 2, rect.width, rowHeight); } // Draw all items @@ -283,9 +288,23 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, // Draw name auto itemName = rowTitle(i); + const bool isSeparator = UITheme::isSeparatorTitle(itemName); + if (isSeparator) { + itemName = UITheme::stripSeparatorTitle(itemName); + } + + if (isSeparator) { + drawListSeparator(renderer, + Rect{rect.x + BaseMetrics::values.contentSidePadding, itemY, + contentWidth - BaseMetrics::values.contentSidePadding * 2, rowHeight}, + rect.x + BaseMetrics::values.contentSidePadding, textWidth, itemName); + continue; + } + auto font = (rowSubtitle != nullptr) ? UI_12_FONT_ID : UI_10_FONT_ID; auto item = renderer.truncatedText(font, itemName.c_str(), textWidth); - renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding, itemY, item.c_str(), i != selectedIndex); + renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding, itemY, item.c_str(), true); + renderer.drawText(font, rect.x + BaseMetrics::values.contentSidePadding, itemY, item.c_str(), true); if (rowSubtitle != nullptr) { // Draw subtitle; if the text is newline-separated (author\nseries), join with • for single-line display @@ -309,6 +328,14 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, } } +void BaseTheme::drawListSeparator(const GfxRenderer& renderer, Rect rowRect, int textX, int textWidth, + const std::string& title) const { + const std::string item = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), textWidth); + const int lineY = rowRect.y + rowRect.height - 2; + renderer.drawLine(rowRect.x, lineY, rowRect.x + rowRect.width - 1, lineY, true); + renderer.drawText(SMALL_FONT_ID, textX, rowRect.y + 7, item.c_str(), true, EpdFontFamily::BOLD); +} + void BaseTheme::drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle) const { // Hide last battery draw constexpr int maxBatteryWidth = 80; diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index daa79c0c..e5ddc5d6 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -121,6 +121,8 @@ class BaseTheme { const std::function& rowIcon = nullptr, const std::function& rowValue = nullptr, bool highlightValue = false) const; + virtual void drawListSeparator(const GfxRenderer& renderer, Rect rowRect, int textX, int textWidth, + const std::string& title) const; virtual void drawHeader(const GfxRenderer& renderer, Rect rect, const char* title, const char* subtitle = nullptr) const; virtual void drawSubHeader(const GfxRenderer& renderer, Rect rect, const char* label, diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index d8f454b2..a7ff264d 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -401,11 +401,16 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, scrollBarHeight, true); } + bool selectedIsSeparator = false; + if (selectedIndex >= 0 && selectedIndex < itemCount && rowTitle != nullptr) { + selectedIsSeparator = UITheme::isSeparatorTitle(rowTitle(selectedIndex)); + } + // Draw selection int contentWidth = rect.width - (totalPages > 1 ? (LyraMetrics::values.scrollBarWidth + LyraMetrics::values.scrollBarRightOffset) : 1); - if (selectedIndex >= 0) { + if (selectedIndex >= 0 && !selectedIsSeparator) { renderer.fillRoundedRect( rect.x + LyraMetrics::values.contentSidePadding, rect.y + selectedIndex % pageItems * rowHeight, contentWidth - LyraMetrics::values.contentSidePadding * 2, rowHeight, cornerRadius, Color::LightGray); @@ -438,6 +443,19 @@ void LyraTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, } auto itemName = rowTitle(i); + const bool isSeparator = UITheme::isSeparatorTitle(itemName); + if (isSeparator) { + itemName = UITheme::stripSeparatorTitle(itemName); + } + + if (isSeparator) { + drawListSeparator(renderer, + Rect{rect.x + LyraMetrics::values.contentSidePadding, itemY, + contentWidth - LyraMetrics::values.contentSidePadding * 2, rowHeight}, + textX, rowTextWidth, itemName); + continue; + } + 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); diff --git a/src/util/ButtonNavigator.cpp b/src/util/ButtonNavigator.cpp index d9844138..e8e8b499 100644 --- a/src/util/ButtonNavigator.cpp +++ b/src/util/ButtonNavigator.cpp @@ -73,6 +73,26 @@ bool ButtonNavigator::shouldNavigateContinuously() const { return buttonHeldLongEnough && navigationIntervalElapsed; } +void ButtonNavigator::setSelectablePredicate(std::function selectablePredicate, int totalItems) { + this->selectablePredicate = std::move(selectablePredicate); + this->selectableTotalItems = totalItems; +} + +void ButtonNavigator::clearSelectablePredicate() { + selectablePredicate = nullptr; + selectableTotalItems = 0; +} + +int ButtonNavigator::nextIndex(int currentIndex) const { + if (!selectablePredicate || selectableTotalItems <= 0) return currentIndex; + return nextIndex(currentIndex, selectableTotalItems, selectablePredicate); +} + +int ButtonNavigator::previousIndex(int currentIndex) const { + if (!selectablePredicate || selectableTotalItems <= 0) return currentIndex; + return previousIndex(currentIndex, selectableTotalItems, selectablePredicate); +} + int ButtonNavigator::nextIndex(const int currentIndex, const int totalItems) { if (totalItems <= 0) return 0; @@ -87,6 +107,68 @@ int ButtonNavigator::previousIndex(const int currentIndex, const int totalItems) return (currentIndex + totalItems - 1) % totalItems; } +int ButtonNavigator::nextIndex(const int currentIndex, const std::vector& selectable) { + const int totalItems = static_cast(selectable.size()); + if (totalItems <= 0) return 0; + + int index = nextIndex(currentIndex, totalItems); + for (int i = 0; i < totalItems; ++i) { + if (selectable[index]) { + return index; + } + index = nextIndex(index, totalItems); + } + + return currentIndex; +} + +int ButtonNavigator::previousIndex(const int currentIndex, const std::vector& selectable) { + const int totalItems = static_cast(selectable.size()); + if (totalItems <= 0) return 0; + + int index = previousIndex(currentIndex, totalItems); + for (int i = 0; i < totalItems; ++i) { + if (selectable[index]) { + return index; + } + index = previousIndex(index, totalItems); + } + + return currentIndex; +} + +int ButtonNavigator::nextIndex(const int currentIndex, const int totalItems, + const std::function& isSelectable) { + if (totalItems <= 0) return 0; + if (!isSelectable) return nextIndex(currentIndex, totalItems); + + int index = nextIndex(currentIndex, totalItems); + for (int i = 0; i < totalItems; ++i) { + if (isSelectable(index)) { + return index; + } + index = nextIndex(index, totalItems); + } + + return currentIndex; +} + +int ButtonNavigator::previousIndex(const int currentIndex, const int totalItems, + const std::function& isSelectable) { + if (totalItems <= 0) return 0; + if (!isSelectable) return previousIndex(currentIndex, totalItems); + + int index = previousIndex(currentIndex, totalItems); + for (int i = 0; i < totalItems; ++i) { + if (isSelectable(index)) { + return index; + } + index = previousIndex(index, totalItems); + } + + return currentIndex; +} + int ButtonNavigator::nextPageIndex(const int currentIndex, const int totalItems, const int itemsPerPage) { if (totalItems <= 0 || itemsPerPage <= 0) return 0; diff --git a/src/util/ButtonNavigator.h b/src/util/ButtonNavigator.h index 2f9afbc1..11a17449 100644 --- a/src/util/ButtonNavigator.h +++ b/src/util/ButtonNavigator.h @@ -13,6 +13,8 @@ class ButtonNavigator final { const uint16_t continuousIntervalMs; uint32_t lastContinuousNavTime = 0; static const MappedInputManager* mappedInput; + std::function selectablePredicate; + int selectableTotalItems = 0; [[nodiscard]] bool shouldNavigateContinuously() const; @@ -40,6 +42,17 @@ class ButtonNavigator final { [[nodiscard]] static int nextIndex(int currentIndex, int totalItems); [[nodiscard]] static int previousIndex(int currentIndex, int totalItems); + [[nodiscard]] static int nextIndex(int currentIndex, const std::vector& selectable); + [[nodiscard]] static int previousIndex(int currentIndex, const std::vector& selectable); + [[nodiscard]] static int nextIndex(int currentIndex, int totalItems, + const std::function& isSelectable); + [[nodiscard]] static int previousIndex(int currentIndex, int totalItems, + const std::function& isSelectable); + + [[nodiscard]] int nextIndex(int currentIndex) const; + [[nodiscard]] int previousIndex(int currentIndex) const; + void setSelectablePredicate(std::function selectablePredicate, int totalItems); + void clearSelectablePredicate(); [[nodiscard]] static int nextPageIndex(int currentIndex, int totalItems, int itemsPerPage); [[nodiscard]] static int previousPageIndex(int currentIndex, int totalItems, int itemsPerPage);