diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index 3a6f780d..b453bae4 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -465,4 +465,7 @@ STR_SLEEP_SCREEN_SET: "Sleep screen updated!" STR_IMAGE_DISPLAY_BW: ">> B&W" STR_IMAGE_DISPLAY_GRAYSCALE: ">> Gray" STR_WEATHER_MOON_INFO: "Moon" -STR_WEATHER_SUN_INFO: "Sun" \ No newline at end of file +STR_WEATHER_SUN_INFO: "Sun" +STR_READER_TOOLS: "Tools" +STR_READER_NAVIGATION: "Navigation" +STR_READER_APPEARANCE: "Appearance" \ No newline at end of file diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index c1bf98b2..313f4890 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -27,30 +27,52 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu std::vector EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) { std::vector items; - items.reserve(13); + items.reserve(18); + // Navigation + items.push_back({MenuAction::NONE, StrId::STR_READER_NAVIGATION, true}); items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}); + items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}); if (hasFootnotes) { items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES}); } + items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN}); + + // Appearance + items.push_back({MenuAction::NONE, StrId::STR_READER_APPEARANCE, true}); items.push_back({MenuAction::EMBEDDED_STYLE, StrId::STR_EMBEDDED_STYLE}); items.push_back({MenuAction::IMAGE_RENDERING, StrId::STR_IMAGES}); items.push_back({MenuAction::TEXT_DARKNESS, StrId::STR_TEXT_DARKNESS}); items.push_back({MenuAction::ROTATE_SCREEN, StrId::STR_ORIENTATION}); - items.push_back({MenuAction::AUTO_PAGE_TURN, StrId::STR_AUTO_TURN_PAGES_PER_MIN}); - items.push_back({MenuAction::GO_TO_PERCENT, StrId::STR_GO_TO_PERCENT}); - items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON}); - items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR}); - items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON}); + + // Synchronisation (only if credentials are set, to avoid confusion) if (KOREADER_STORE.hasCredentials()) { + items.push_back({MenuAction::NONE, StrId::STR_KOREADER_SYNC, true}); items.push_back({MenuAction::PULL_REMOTE, StrId::STR_PULL_PROGRESS_FROM_OTHER_DEVICES}); items.push_back({MenuAction::PUSH_LOCAL, StrId::STR_PUSH_PROGRESS_FROM_THIS_DEVICE}); } + + // Tools + items.push_back({MenuAction::NONE, StrId::STR_READER_TOOLS, true}); + items.push_back({MenuAction::SCREENSHOT, StrId::STR_SCREENSHOT_BUTTON}); + items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR}); items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}); + items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON}); return items; } +std::function EpubReaderMenuActivity::buildSelectablePredicate() const { + return [this](int index) { + return index >= 0 && index < static_cast(menuItems.size()) && !menuItems[index].isSeparator; + }; +} + void EpubReaderMenuActivity::onEnter() { Activity::onEnter(); + const auto selectablePredicate = buildSelectablePredicate(); + buttonNavigator.setSelectablePredicate(selectablePredicate, static_cast(menuItems.size())); + if (!selectablePredicate(selectedIndex)) { + selectedIndex = buttonNavigator.nextIndex(selectedIndex); + } requestUpdate(); } @@ -59,17 +81,20 @@ void EpubReaderMenuActivity::onExit() { Activity::onExit(); } void EpubReaderMenuActivity::loop() { // Handle navigation buttonNavigator.onNext([this] { - selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast(menuItems.size())); + selectedIndex = buttonNavigator.nextIndex(selectedIndex); requestUpdate(); }); buttonNavigator.onPrevious([this] { - selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast(menuItems.size())); + selectedIndex = buttonNavigator.previousIndex(selectedIndex); requestUpdate(); }); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { const auto selectedAction = menuItems[selectedIndex].action; + if (selectedAction == MenuAction::NONE) { + return; + } if (selectedAction == MenuAction::ROTATE_SCREEN) { // Cycle orientation preview locally; actual rotation happens on menu exit. pendingOrientation = (pendingOrientation + 1) % orientationLabels.size(); @@ -158,60 +183,45 @@ void EpubReaderMenuActivity::render(RenderLock&&) { // Menu Items const int startY = 75 + contentRect.y; - constexpr int lineHeight = 30; + const int listHeight = contentRect.height - (startY - contentRect.y); - for (size_t i = 0; i < menuItems.size(); ++i) { - const int displayY = startY + (i * lineHeight); - const bool isSelected = (static_cast(i) == selectedIndex); - - if (isSelected) { - // Highlight only the content area so we don't paint over hint gutters. - renderer.fillRect(contentRect.x, displayY, contentRect.width - 1, lineHeight, true); - } - - renderer.drawText(UI_10_FONT_ID, contentRect.x + 20, displayY, I18N.get(menuItems[i].labelId), !isSelected); - - if (menuItems[i].action == MenuAction::ROTATE_SCREEN) { - // Render current orientation value on the right edge of the content area. - const char* value = I18N.get(orientationLabels[pendingOrientation]); - const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); - renderer.drawText(UI_10_FONT_ID, contentRect.x + contentRect.width - 20 - width, displayY, value, !isSelected); - } - - if (menuItems[i].action == MenuAction::AUTO_PAGE_TURN) { - // Render current page turn value on the right edge of the content area. - const auto value = pageTurnLabels[selectedPageTurnOption]; - const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); - renderer.drawText(UI_10_FONT_ID, contentRect.x + contentRect.width - 20 - width, displayY, value, !isSelected); - } - - if (menuItems[i].action == MenuAction::EMBEDDED_STYLE) { - const char* value = tr(STR_DEFAULT_VALUE); - if (pendingEmbeddedStyleOverride == 1) { - value = tr(STR_STATE_ON); - } else if (pendingEmbeddedStyleOverride == 0) { - value = tr(STR_STATE_OFF); - } - const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); - renderer.drawText(UI_10_FONT_ID, contentRect.x + contentRect.width - 20 - width, displayY, value, !isSelected); - } - - if (menuItems[i].action == MenuAction::IMAGE_RENDERING) { - const char* value = tr(STR_DEFAULT_VALUE); - if (pendingImageRenderingOverride >= 0 && pendingImageRenderingOverride < imageRenderingLabels.size()) { - value = I18N.get(imageRenderingLabels[pendingImageRenderingOverride]); - } - const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); - renderer.drawText(UI_10_FONT_ID, contentRect.x + contentRect.width - 20 - width, displayY, value, !isSelected); - } - - if (menuItems[i].action == MenuAction::TEXT_DARKNESS) { - const uint8_t idx = (pendingTextDarkness < textDarknessLabels.size()) ? pendingTextDarkness : 0; - const char* value = I18N.get(textDarknessLabels[idx]); - const auto width = renderer.getTextWidth(UI_10_FONT_ID, value); - renderer.drawText(UI_10_FONT_ID, contentRect.x + contentRect.width - 20 - width, displayY, value, !isSelected); - } - } + GUI.drawList( + renderer, Rect{contentRect.x, startY, contentRect.width, listHeight}, static_cast(menuItems.size()), + selectedIndex, + [this](int index) { + const auto& item = menuItems[index]; + const auto title = I18N.get(item.labelId); + return item.isSeparator ? UITheme::makeSeparatorTitle(title) : title; + }, + nullptr, nullptr, + [this](int index) { + const auto& item = menuItems[index]; + switch (item.action) { + case MenuAction::ROTATE_SCREEN: + return std::string(I18N.get(orientationLabels[pendingOrientation])); + case MenuAction::AUTO_PAGE_TURN: + return std::string(pageTurnLabels[selectedPageTurnOption]); + case MenuAction::EMBEDDED_STYLE: + if (pendingEmbeddedStyleOverride == 1) { + return std::string(tr(STR_STATE_ON)); + } else if (pendingEmbeddedStyleOverride == 0) { + return std::string(tr(STR_STATE_OFF)); + } + return std::string(tr(STR_DEFAULT_VALUE)); + case MenuAction::IMAGE_RENDERING: + if (pendingImageRenderingOverride >= 0 && pendingImageRenderingOverride < imageRenderingLabels.size()) { + return std::string(I18N.get(imageRenderingLabels[pendingImageRenderingOverride])); + } + return std::string(tr(STR_DEFAULT_VALUE)); + case MenuAction::TEXT_DARKNESS: { + const uint8_t idx = (pendingTextDarkness < textDarknessLabels.size()) ? pendingTextDarkness : 0; + return std::string(I18N.get(textDarknessLabels[idx])); + } + default: + return std::string(); + } + }, + true); // Footer / Hints const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index a268cf73..93bd66d8 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -12,6 +12,7 @@ class EpubReaderMenuActivity final : public Activity { public: // Menu actions available from the reader menu. enum class MenuAction { + NONE, SELECT_CHAPTER, FOOTNOTES, EMBEDDED_STYLE, @@ -43,10 +44,13 @@ class EpubReaderMenuActivity final : public Activity { struct MenuItem { MenuAction action; StrId labelId; + bool isSeparator = false; }; static std::vector buildMenuItems(bool hasFootnotes); + std::function buildSelectablePredicate() const; + // Fixed menu layout const std::vector menuItems; diff --git a/src/activities/settings/ClockSettingsActivity.cpp b/src/activities/settings/ClockSettingsActivity.cpp index b79a7bc8..9b99f3a8 100644 --- a/src/activities/settings/ClockSettingsActivity.cpp +++ b/src/activities/settings/ClockSettingsActivity.cpp @@ -12,10 +12,6 @@ #include "fontIds.h" namespace { -constexpr int MENU_ITEMS = 5; -const StrId menuNames[MENU_ITEMS] = {StrId::STR_USE_CLOCK, StrId::STR_CLOCK_FORMAT, StrId::STR_TIMEZONE, - StrId::STR_SYNC_TIME, StrId::STR_DETECT_TIMEZONE}; - const StrId timeZoneNames[CrossPointSettings::TIMEZONE_COUNT] = { StrId::STR_TZ_UTC, StrId::STR_TZ_CET, StrId::STR_TZ_EET, StrId::STR_TZ_MSK, StrId::STR_TZ_UTC_PLUS4, StrId::STR_TZ_IST, StrId::STR_TZ_UTC_PLUS7, StrId::STR_TZ_UTC_PLUS8, @@ -23,9 +19,34 @@ const StrId timeZoneNames[CrossPointSettings::TIMEZONE_COUNT] = { StrId::STR_TZ_EST, StrId::STR_TZ_CST, StrId::STR_TZ_MST, StrId::STR_TZ_PST}; } // namespace +std::vector ClockSettingsActivity::buildMenuItems() { + std::vector items; + items.reserve(7); + // Settings + items.push_back({Action::NONE, StrId::STR_SETTINGS_TITLE, true}); + items.push_back({Action::USE_CLOCK, StrId::STR_USE_CLOCK}); + items.push_back({Action::CLOCK_FORMAT, StrId::STR_CLOCK_FORMAT}); + items.push_back({Action::TIMEZONE, StrId::STR_TIMEZONE}); + + // Tools + items.push_back({Action::NONE, StrId::STR_READER_TOOLS, true}); + items.push_back({Action::DETECT_TIMEZONE, StrId::STR_DETECT_TIMEZONE}); + items.push_back({Action::SYNC_TIME, StrId::STR_SYNC_TIME}); + return items; +} + +std::function ClockSettingsActivity::buildSelectablePredicate() const { + return [this](int index) { + return index >= 0 && index < static_cast(menuItems.size()) && !menuItems[index].isSeparator; + }; +} + void ClockSettingsActivity::onEnter() { Activity::onEnter(); - selectedIndex = 0; + buttonNavigator.setSelectablePredicate(buildSelectablePredicate(), static_cast(menuItems.size())); + if (!buildSelectablePredicate()(selectedIndex)) { + selectedIndex = buttonNavigator.nextIndex(selectedIndex); + } requestUpdate(); } @@ -44,44 +65,45 @@ void ClockSettingsActivity::loop() { } buttonNavigator.onNextRelease([this] { - selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEMS); + selectedIndex = buttonNavigator.nextIndex(selectedIndex); requestUpdate(); }); buttonNavigator.onPreviousRelease([this] { - selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEMS); + selectedIndex = buttonNavigator.previousIndex(selectedIndex); requestUpdate(); }); buttonNavigator.onNextContinuous([this] { - selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEMS); + selectedIndex = buttonNavigator.nextIndex(selectedIndex); requestUpdate(); }); buttonNavigator.onPreviousContinuous([this] { - selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEMS); + selectedIndex = buttonNavigator.previousIndex(selectedIndex); requestUpdate(); }); } void ClockSettingsActivity::handleSelection() { - if (selectedIndex == 0) { + const auto action = menuItems[selectedIndex].action; + if (action == Action::USE_CLOCK) { SETTINGS.useClock = (SETTINGS.useClock + 1) % 2; if (!SETTINGS.useClock) { SETTINGS.statusBarClock = 0; } SETTINGS.saveToFile(); - } else if (selectedIndex == 1) { + } else if (action == Action::CLOCK_FORMAT) { SETTINGS.clockFormat12h = (SETTINGS.clockFormat12h + 1) % 2; SETTINGS.saveToFile(); - } else if (selectedIndex == 2) { + } else if (action == Action::TIMEZONE) { SETTINGS.timeZone = (SETTINGS.timeZone + 1) % CrossPointSettings::TIMEZONE_COUNT; HalClock::applyTimezone(SETTINGS.timeZone); SETTINGS.saveToFile(); - } else if (selectedIndex == 3) { + } else if (action == Action::SYNC_TIME) { auto resultHandler = [](const ActivityResult&) { SETTINGS.saveToFile(); }; startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); - } else if (selectedIndex == 4) { + } else if (action == Action::DETECT_TIMEZONE) { auto resultHandler = [](const ActivityResult&) { SETTINGS.saveToFile(); }; startActivityForResult(std::make_unique(renderer, mappedInput), resultHandler); } @@ -102,23 +124,29 @@ void ClockSettingsActivity::render(RenderLock&&) { const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; GUI.drawList( - renderer, Rect{0, contentTop, pageWidth, contentHeight}, MENU_ITEMS, selectedIndex, - [](int index) { return std::string(I18N.get(menuNames[index])); }, nullptr, nullptr, - [](int index) { - if (index == 0) { - return std::string(SETTINGS.useClock ? tr(STR_STATE_ON) : tr(STR_STATE_OFF)); - } - if (index == 1) { - return std::string(SETTINGS.clockFormat12h ? tr(STR_12H) : tr(STR_24H)); - } - if (index == 2) { - const auto tzIndex = static_cast(SETTINGS.timeZone); - if (tzIndex < (sizeof(timeZoneNames) / sizeof(timeZoneNames[0]))) { - return std::string(I18N.get(timeZoneNames[tzIndex])); + renderer, Rect{0, contentTop, pageWidth, contentHeight}, static_cast(menuItems.size()), selectedIndex, + [this](int index) { + const auto title = I18N.get(menuItems[index].labelId); + return menuItems[index].isSeparator ? UITheme::makeSeparatorTitle(title) : title; + }, + nullptr, nullptr, + [this](int index) { + const auto action = menuItems[index].action; + switch (action) { + case Action::USE_CLOCK: + return std::string(SETTINGS.useClock ? tr(STR_STATE_ON) : tr(STR_STATE_OFF)); + case Action::CLOCK_FORMAT: + return std::string(SETTINGS.clockFormat12h ? tr(STR_12H) : tr(STR_24H)); + case Action::TIMEZONE: { + const auto tzIndex = static_cast(SETTINGS.timeZone); + if (tzIndex < (sizeof(timeZoneNames) / sizeof(timeZoneNames[0]))) { + return std::string(I18N.get(timeZoneNames[tzIndex])); + } + return std::string(tr(STR_TZ_UTC)); } - return std::string(tr(STR_TZ_UTC)); + default: + return std::string(""); } - return std::string(""); }, true); diff --git a/src/activities/settings/ClockSettingsActivity.h b/src/activities/settings/ClockSettingsActivity.h index e1f491bc..ffeea5d9 100644 --- a/src/activities/settings/ClockSettingsActivity.h +++ b/src/activities/settings/ClockSettingsActivity.h @@ -1,18 +1,32 @@ #pragma once #include +#include +#include + #include "activities/Activity.h" #include "util/ButtonNavigator.h" class ClockSettingsActivity final : public Activity { + enum class Action { USE_CLOCK, CLOCK_FORMAT, TIMEZONE, SYNC_TIME, DETECT_TIMEZONE, NONE }; + + struct MenuItem { + Action action; + StrId labelId; + bool isSeparator = false; + }; + ButtonNavigator buttonNavigator; int selectedIndex = 0; + const std::vector menuItems; + static std::vector buildMenuItems(); void handleSelection(); + std::function buildSelectablePredicate() const; public: explicit ClockSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) - : Activity("ClockSettings", renderer, mappedInput) {} + : Activity("ClockSettings", renderer, mappedInput), menuItems(buildMenuItems()) {} void onEnter() override; void onExit() override; void loop() override; diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 4a61f00d..67a09e16 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -97,6 +98,16 @@ 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..abdc1906 100644 --- a/src/components/UITheme.h +++ b/src/components/UITheme.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include @@ -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 c982982b..65608a75 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -270,10 +270,15 @@ 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) { - renderer.fillRect(0, rect.y + selectedIndex % pageItems * rowHeight - 2, rect.width, rowHeight); + if (selectedIndex >= 0 && !selectedIsSeparator) { + renderer.fillRect(rect.x, rect.y + selectedIndex % pageItems * rowHeight - 2, rect.width, rowHeight); } // Draw all items const auto pageStartIndex = selectedIndex / pageItems * pageItems; @@ -283,9 +288,19 @@ 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); + 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); if (rowSubtitle != nullptr) { // Draw subtitle; if the text is newline-separated (author\nseries), join with • for single-line display @@ -309,6 +324,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..7658356f 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,16 @@ 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); + 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..b76df714 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);