A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: unify navigation handling with system-wide continuous navigation (#600)

This PR unifies navigation handling & adds system-wide support for
continuous navigation.

## Summary
Holding down a navigation button now continuously advances through items
until the button is released. This removes the need for repeated
press-and-release actions and makes navigation faster and smoother,
especially in long menus or documents.

When page-based navigation is available, it will navigate through pages.
If not, it will progress through menu items or similar list-based UI
elements.

Additionally, this PR fixes inconsistencies in wrap-around behavior and
navigation index calculations.

Places where the navigation system was updated:
- Home Page
- Settings Pages
- My Library Page
- WiFi Selection Page
- OPDS Browser Page
- Keyboard
- File Transfer Page
- XTC Chapter Selector Page
- EPUB Chapter Selector Page

I’ve tested this on the device as much as possible and tried to match
the existing behavior. Please let me know if I missed anything. Thanks 🙏


![crosspoint](https://github.com/user-attachments/assets/6a3c7482-f45e-4a77-b156-721bb3b679e6)

---

Following the request from @osteotek and @daveallie for system-wide
support, the old PR (#379) has been closed in favor of this
consolidated, system-wide implementation.

---

### AI Usage

Did you use AI tools to help write this code? _**PARTIALLY**_

---------

Co-authored-by: Dave Allie <dave@daveallie.com>

authored by

Istiak Tridip
Dave Allie
and committed by
GitHub
64d161e8 e73bb321

+403 -261
+23 -21
src/activities/browser/OpdsBookBrowserActivity.cpp
··· 17 17 18 18 namespace { 19 19 constexpr int PAGE_ITEMS = 23; 20 - constexpr int SKIP_PAGE_MS = 700; 21 20 } // namespace 22 21 23 22 void OpdsBookBrowserActivity::taskTrampoline(void* param) { ··· 118 117 119 118 // Handle browsing state 120 119 if (state == BrowserState::BROWSING) { 121 - const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || 122 - mappedInput.wasReleased(MappedInputManager::Button::Left); 123 - const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || 124 - mappedInput.wasReleased(MappedInputManager::Button::Right); 125 - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; 126 - 127 120 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 128 121 if (!entries.empty()) { 129 122 const auto& entry = entries[selectorIndex]; ··· 135 128 } 136 129 } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { 137 130 navigateBack(); 138 - } else if (prevReleased && !entries.empty()) { 139 - if (skipPage) { 140 - selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + entries.size()) % entries.size(); 141 - } else { 142 - selectorIndex = (selectorIndex + entries.size() - 1) % entries.size(); 143 - } 144 - updateRequired = true; 145 - } else if (nextReleased && !entries.empty()) { 146 - if (skipPage) { 147 - selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % entries.size(); 148 - } else { 149 - selectorIndex = (selectorIndex + 1) % entries.size(); 150 - } 151 - updateRequired = true; 131 + } 132 + 133 + // Handle navigation 134 + if (!entries.empty()) { 135 + buttonNavigator.onNextRelease([this] { 136 + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entries.size()); 137 + updateRequired = true; 138 + }); 139 + 140 + buttonNavigator.onPreviousRelease([this] { 141 + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entries.size()); 142 + updateRequired = true; 143 + }); 144 + 145 + buttonNavigator.onNextContinuous([this] { 146 + selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entries.size(), PAGE_ITEMS); 147 + updateRequired = true; 148 + }); 149 + 150 + buttonNavigator.onPreviousContinuous([this] { 151 + selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entries.size(), PAGE_ITEMS); 152 + updateRequired = true; 153 + }); 152 154 } 153 155 } 154 156 }
+2
src/activities/browser/OpdsBookBrowserActivity.h
··· 9 9 #include <vector> 10 10 11 11 #include "../ActivityWithSubactivity.h" 12 + #include "util/ButtonNavigator.h" 12 13 13 14 /** 14 15 * Activity for browsing and downloading books from an OPDS server. ··· 37 38 private: 38 39 TaskHandle_t displayTaskHandle = nullptr; 39 40 SemaphoreHandle_t renderingMutex = nullptr; 41 + ButtonNavigator buttonNavigator; 40 42 bool updateRequired = false; 41 43 42 44 BrowserState state = BrowserState::LOADING;
+10 -11
src/activities/home/HomeActivity.cpp
··· 196 196 } 197 197 198 198 void HomeActivity::loop() { 199 - const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) || 200 - mappedInput.wasPressed(MappedInputManager::Button::Left); 201 - const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) || 202 - mappedInput.wasPressed(MappedInputManager::Button::Right); 199 + const int menuCount = getMenuItemCount(); 200 + 201 + buttonNavigator.onNext([this, menuCount] { 202 + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, menuCount); 203 + updateRequired = true; 204 + }); 203 205 204 - const int menuCount = getMenuItemCount(); 206 + buttonNavigator.onPrevious([this, menuCount] { 207 + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, menuCount); 208 + updateRequired = true; 209 + }); 205 210 206 211 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 207 212 // Calculate dynamic indices based on which options are available ··· 226 231 } else if (menuSelectedIndex == settingsIdx) { 227 232 onSettingsOpen(); 228 233 } 229 - } else if (prevPressed) { 230 - selectorIndex = (selectorIndex + menuCount - 1) % menuCount; 231 - updateRequired = true; 232 - } else if (nextPressed) { 233 - selectorIndex = (selectorIndex + 1) % menuCount; 234 - updateRequired = true; 235 234 } 236 235 } 237 236
+2
src/activities/home/HomeActivity.h
··· 8 8 9 9 #include "../Activity.h" 10 10 #include "./MyLibraryActivity.h" 11 + #include "util/ButtonNavigator.h" 11 12 12 13 struct RecentBook; 13 14 struct Rect; ··· 15 16 class HomeActivity final : public Activity { 16 17 TaskHandle_t displayTaskHandle = nullptr; 17 18 SemaphoreHandle_t renderingMutex = nullptr; 19 + ButtonNavigator buttonNavigator; 18 20 int selectorIndex = 0; 19 21 bool updateRequired = false; 20 22 bool recentsLoading = false;
+19 -22
src/activities/home/MyLibraryActivity.cpp
··· 11 11 #include "util/StringUtils.h" 12 12 13 13 namespace { 14 - constexpr int SKIP_PAGE_MS = 700; 15 14 constexpr unsigned long GO_HOME_MS = 1000; 16 15 } // namespace 17 16 ··· 109 108 return; 110 109 } 111 110 112 - const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left) || 113 - mappedInput.wasReleased(MappedInputManager::Button::Up); 114 - ; 115 - const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Right) || 116 - mappedInput.wasReleased(MappedInputManager::Button::Down); 117 - 118 - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; 119 111 const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false); 120 112 121 113 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { ··· 157 149 } 158 150 159 151 int listSize = static_cast<int>(files.size()); 160 - if (upReleased) { 161 - if (skipPage) { 162 - selectorIndex = std::max(static_cast<int>((selectorIndex / pageItems - 1) * pageItems), 0); 163 - } else { 164 - selectorIndex = (selectorIndex + listSize - 1) % listSize; 165 - } 152 + 153 + buttonNavigator.onNextRelease([this, listSize] { 154 + selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize); 155 + updateRequired = true; 156 + }); 157 + 158 + buttonNavigator.onPreviousRelease([this, listSize] { 159 + selectorIndex = ButtonNavigator::previousIndex(static_cast<int>(selectorIndex), listSize); 160 + updateRequired = true; 161 + }); 162 + 163 + buttonNavigator.onNextContinuous([this, listSize, pageItems] { 164 + selectorIndex = ButtonNavigator::nextPageIndex(static_cast<int>(selectorIndex), listSize, pageItems); 166 165 updateRequired = true; 167 - } else if (downReleased) { 168 - if (skipPage) { 169 - selectorIndex = std::min(static_cast<int>((selectorIndex / pageItems + 1) * pageItems), listSize - 1); 170 - } else { 171 - selectorIndex = (selectorIndex + 1) % listSize; 172 - } 166 + }); 167 + 168 + buttonNavigator.onPreviousContinuous([this, listSize, pageItems] { 169 + selectorIndex = ButtonNavigator::previousPageIndex(static_cast<int>(selectorIndex), listSize, pageItems); 173 170 updateRequired = true; 174 - } 171 + }); 175 172 } 176 173 177 174 void MyLibraryActivity::displayTaskLoop() { ··· 217 214 for (size_t i = 0; i < files.size(); i++) 218 215 if (files[i] == name) return i; 219 216 return 0; 220 - } 217 + }
+3
src/activities/home/MyLibraryActivity.h
··· 8 8 #include <vector> 9 9 10 10 #include "../Activity.h" 11 + #include "RecentBooksStore.h" 12 + #include "util/ButtonNavigator.h" 11 13 12 14 class MyLibraryActivity final : public Activity { 13 15 private: 14 16 TaskHandle_t displayTaskHandle = nullptr; 15 17 SemaphoreHandle_t renderingMutex = nullptr; 18 + ButtonNavigator buttonNavigator; 16 19 17 20 size_t selectorIndex = 0; 18 21 bool updateRequired = false;
+18 -21
src/activities/home/RecentBooksActivity.cpp
··· 12 12 #include "util/StringUtils.h" 13 13 14 14 namespace { 15 - constexpr int SKIP_PAGE_MS = 700; 16 15 constexpr unsigned long GO_HOME_MS = 1000; 17 16 } // namespace 18 17 ··· 70 69 } 71 70 72 71 void RecentBooksActivity::loop() { 73 - const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left) || 74 - mappedInput.wasReleased(MappedInputManager::Button::Up); 75 - ; 76 - const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Right) || 77 - mappedInput.wasReleased(MappedInputManager::Button::Down); 78 - 79 - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; 80 72 const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, true); 81 73 82 74 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { ··· 92 84 } 93 85 94 86 int listSize = static_cast<int>(recentBooks.size()); 95 - if (upReleased) { 96 - if (skipPage) { 97 - selectorIndex = std::max(static_cast<int>((selectorIndex / pageItems - 1) * pageItems), 0); 98 - } else { 99 - selectorIndex = (selectorIndex + listSize - 1) % listSize; 100 - } 87 + 88 + buttonNavigator.onNextRelease([this, listSize] { 89 + selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize); 101 90 updateRequired = true; 102 - } else if (downReleased) { 103 - if (skipPage) { 104 - selectorIndex = std::min(static_cast<int>((selectorIndex / pageItems + 1) * pageItems), listSize - 1); 105 - } else { 106 - selectorIndex = (selectorIndex + 1) % listSize; 107 - } 91 + }); 92 + 93 + buttonNavigator.onPreviousRelease([this, listSize] { 94 + selectorIndex = ButtonNavigator::previousIndex(static_cast<int>(selectorIndex), listSize); 95 + updateRequired = true; 96 + }); 97 + 98 + buttonNavigator.onNextContinuous([this, listSize, pageItems] { 99 + selectorIndex = ButtonNavigator::nextPageIndex(static_cast<int>(selectorIndex), listSize, pageItems); 100 + updateRequired = true; 101 + }); 102 + 103 + buttonNavigator.onPreviousContinuous([this, listSize, pageItems] { 104 + selectorIndex = ButtonNavigator::previousPageIndex(static_cast<int>(selectorIndex), listSize, pageItems); 108 105 updateRequired = true; 109 - } 106 + }); 110 107 } 111 108 112 109 void RecentBooksActivity::displayTaskLoop() {
+2
src/activities/home/RecentBooksActivity.h
··· 9 9 10 10 #include "../Activity.h" 11 11 #include "RecentBooksStore.h" 12 + #include "util/ButtonNavigator.h" 12 13 13 14 class RecentBooksActivity final : public Activity { 14 15 private: 15 16 TaskHandle_t displayTaskHandle = nullptr; 16 17 SemaphoreHandle_t renderingMutex = nullptr; 18 + ButtonNavigator buttonNavigator; 17 19 18 20 size_t selectorIndex = 0; 19 21 bool updateRequired = false;
+7 -10
src/activities/network/NetworkModeSelectionActivity.cpp
··· 73 73 } 74 74 75 75 // Handle navigation 76 - const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) || 77 - mappedInput.wasPressed(MappedInputManager::Button::Left); 78 - const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) || 79 - mappedInput.wasPressed(MappedInputManager::Button::Right); 76 + buttonNavigator.onNext([this] { 77 + selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEM_COUNT); 78 + updateRequired = true; 79 + }); 80 80 81 - if (prevPressed) { 82 - selectedIndex = (selectedIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT; 83 - updateRequired = true; 84 - } else if (nextPressed) { 85 - selectedIndex = (selectedIndex + 1) % MENU_ITEM_COUNT; 81 + buttonNavigator.onPrevious([this] { 82 + selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEM_COUNT); 86 83 updateRequired = true; 87 - } 84 + }); 88 85 } 89 86 90 87 void NetworkModeSelectionActivity::displayTaskLoop() {
+3
src/activities/network/NetworkModeSelectionActivity.h
··· 6 6 #include <functional> 7 7 8 8 #include "../Activity.h" 9 + #include "util/ButtonNavigator.h" 9 10 10 11 // Enum for network mode selection 11 12 enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT }; ··· 22 23 class NetworkModeSelectionActivity final : public Activity { 23 24 TaskHandle_t displayTaskHandle = nullptr; 24 25 SemaphoreHandle_t renderingMutex = nullptr; 26 + ButtonNavigator buttonNavigator; 27 + 25 28 int selectedIndex = 0; 26 29 bool updateRequired = false; 27 30 const std::function<void(NetworkMode)> onModeSelected;
+10 -14
src/activities/network/WifiSelectionActivity.cpp
··· 420 420 return; 421 421 } 422 422 423 - // Handle UP/DOWN navigation 424 - if (mappedInput.wasPressed(MappedInputManager::Button::Up) || 425 - mappedInput.wasPressed(MappedInputManager::Button::Left)) { 426 - if (selectedNetworkIndex > 0) { 427 - selectedNetworkIndex--; 428 - updateRequired = true; 429 - } 430 - } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || 431 - mappedInput.wasPressed(MappedInputManager::Button::Right)) { 432 - if (!networks.empty() && selectedNetworkIndex < static_cast<int>(networks.size()) - 1) { 433 - selectedNetworkIndex++; 434 - updateRequired = true; 435 - } 436 - } 423 + // Handle navigation 424 + buttonNavigator.onNext([this] { 425 + selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size()); 426 + updateRequired = true; 427 + }); 428 + 429 + buttonNavigator.onPrevious([this] { 430 + selectedNetworkIndex = ButtonNavigator::previousIndex(selectedNetworkIndex, networks.size()); 431 + updateRequired = true; 432 + }); 437 433 } 438 434 } 439 435
+2
src/activities/network/WifiSelectionActivity.h
··· 10 10 #include <vector> 11 11 12 12 #include "activities/ActivityWithSubactivity.h" 13 + #include "util/ButtonNavigator.h" 13 14 14 15 // Structure to hold WiFi network information 15 16 struct WifiNetworkInfo { ··· 45 46 class WifiSelectionActivity final : public ActivityWithSubactivity { 46 47 TaskHandle_t displayTaskHandle = nullptr; 47 48 SemaphoreHandle_t renderingMutex = nullptr; 49 + ButtonNavigator buttonNavigator; 48 50 bool updateRequired = false; 49 51 WifiSelectionState state = WifiSelectionState::SCANNING; 50 52 int selectedNetworkIndex = 0;
+19 -24
src/activities/reader/EpubReaderChapterSelectionActivity.cpp
··· 6 6 #include "components/UITheme.h" 7 7 #include "fontIds.h" 8 8 9 - namespace { 10 - // Time threshold for treating a long press as a page-up/page-down 11 - constexpr int SKIP_PAGE_MS = 700; 12 - } // namespace 13 - 14 9 int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub->getTocItemsCount(); } 15 10 16 11 int EpubReaderChapterSelectionActivity::getPageItems() const { ··· 77 72 return; 78 73 } 79 74 80 - const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || 81 - mappedInput.wasReleased(MappedInputManager::Button::Left); 82 - const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || 83 - mappedInput.wasReleased(MappedInputManager::Button::Right); 84 - 85 - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; 86 75 const int pageItems = getPageItems(); 87 76 const int totalItems = getTotalItems(); 88 77 ··· 95 84 } 96 85 } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { 97 86 onGoBack(); 98 - } else if (prevReleased) { 99 - if (skipPage) { 100 - selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + totalItems) % totalItems; 101 - } else { 102 - selectorIndex = (selectorIndex + totalItems - 1) % totalItems; 103 - } 87 + } 88 + 89 + buttonNavigator.onNextRelease([this, totalItems] { 90 + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); 104 91 updateRequired = true; 105 - } else if (nextReleased) { 106 - if (skipPage) { 107 - selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % totalItems; 108 - } else { 109 - selectorIndex = (selectorIndex + 1) % totalItems; 110 - } 92 + }); 93 + 94 + buttonNavigator.onPreviousRelease([this, totalItems] { 95 + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); 96 + updateRequired = true; 97 + }); 98 + 99 + buttonNavigator.onNextContinuous([this, totalItems, pageItems] { 100 + selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems); 101 + updateRequired = true; 102 + }); 103 + 104 + buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] { 105 + selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems); 111 106 updateRequired = true; 112 - } 107 + }); 113 108 } 114 109 115 110 void EpubReaderChapterSelectionActivity::displayTaskLoop() {
+2
src/activities/reader/EpubReaderChapterSelectionActivity.h
··· 7 7 #include <memory> 8 8 9 9 #include "../ActivityWithSubactivity.h" 10 + #include "util/ButtonNavigator.h" 10 11 11 12 class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity { 12 13 std::shared_ptr<Epub> epub; 13 14 std::string epubPath; 14 15 TaskHandle_t displayTaskHandle = nullptr; 15 16 SemaphoreHandle_t renderingMutex = nullptr; 17 + ButtonNavigator buttonNavigator; 16 18 int currentSpineIndex = 0; 17 19 int currentPage = 0; 18 20 int totalPagesInSpine = 0;
+11 -8
src/activities/reader/EpubReaderMenuActivity.cpp
··· 48 48 return; 49 49 } 50 50 51 - // Use local variables for items we need to check after potential deletion 52 - if (mappedInput.wasReleased(MappedInputManager::Button::Up) || 53 - mappedInput.wasReleased(MappedInputManager::Button::Left)) { 54 - selectedIndex = (selectedIndex + menuItems.size() - 1) % menuItems.size(); 51 + // Handle navigation 52 + buttonNavigator.onNext([this] { 53 + selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size())); 55 54 updateRequired = true; 56 - } else if (mappedInput.wasReleased(MappedInputManager::Button::Down) || 57 - mappedInput.wasReleased(MappedInputManager::Button::Right)) { 58 - selectedIndex = (selectedIndex + 1) % menuItems.size(); 55 + }); 56 + 57 + buttonNavigator.onPrevious([this] { 58 + selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size())); 59 59 updateRequired = true; 60 - } else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 60 + }); 61 + 62 + // Use local variables for items we need to check after potential deletion 63 + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 61 64 const auto selectedAction = menuItems[selectedIndex].action; 62 65 if (selectedAction == MenuAction::ROTATE_SCREEN) { 63 66 // Cycle orientation preview locally; actual rotation happens on menu exit.
+2
src/activities/reader/EpubReaderMenuActivity.h
··· 9 9 #include <vector> 10 10 11 11 #include "../ActivityWithSubactivity.h" 12 + #include "util/ButtonNavigator.h" 12 13 13 14 class EpubReaderMenuActivity final : public ActivityWithSubactivity { 14 15 public: ··· 48 49 bool updateRequired = false; 49 50 TaskHandle_t displayTaskHandle = nullptr; 50 51 SemaphoreHandle_t renderingMutex = nullptr; 52 + ButtonNavigator buttonNavigator; 51 53 std::string title = "Reader Menu"; 52 54 uint8_t pendingOrientation = 0; 53 55 const std::vector<const char*> orientationLabels = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"};
+4 -18
src/activities/reader/EpubReaderPercentSelectionActivity.cpp
··· 79 79 return; 80 80 } 81 81 82 - if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { 83 - adjustPercent(-kSmallStep); 84 - return; 85 - } 86 - 87 - if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { 88 - adjustPercent(kSmallStep); 89 - return; 90 - } 91 - 92 - if (mappedInput.wasReleased(MappedInputManager::Button::Up)) { 93 - adjustPercent(kLargeStep); 94 - return; 95 - } 82 + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { adjustPercent(-kSmallStep); }); 83 + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { adjustPercent(kSmallStep); }); 96 84 97 - if (mappedInput.wasReleased(MappedInputManager::Button::Down)) { 98 - adjustPercent(-kLargeStep); 99 - return; 100 - } 85 + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { adjustPercent(kLargeStep); }); 86 + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustPercent(-kLargeStep); }); 101 87 } 102 88 103 89 void EpubReaderPercentSelectionActivity::renderScreen() {
+2
src/activities/reader/EpubReaderPercentSelectionActivity.h
··· 7 7 8 8 #include "MappedInputManager.h" 9 9 #include "activities/ActivityWithSubactivity.h" 10 + #include "util/ButtonNavigator.h" 10 11 11 12 class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity { 12 13 public: ··· 31 32 // FreeRTOS task and mutex for rendering. 32 33 TaskHandle_t displayTaskHandle = nullptr; 33 34 SemaphoreHandle_t renderingMutex = nullptr; 35 + ButtonNavigator buttonNavigator; 34 36 35 37 // Callback invoked when the user confirms a percent. 36 38 const std::function<void(int)> onSelect;
+20 -31
src/activities/reader/XtcReaderChapterSelectionActivity.cpp
··· 8 8 #include "components/UITheme.h" 9 9 #include "fontIds.h" 10 10 11 - namespace { 12 - constexpr int SKIP_PAGE_MS = 700; 13 - } // namespace 14 - 15 11 int XtcReaderChapterSelectionActivity::getPageItems() const { 16 12 constexpr int lineHeight = 30; 17 13 ··· 78 74 } 79 75 80 76 void XtcReaderChapterSelectionActivity::loop() { 81 - const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || 82 - mappedInput.wasReleased(MappedInputManager::Button::Left); 83 - const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || 84 - mappedInput.wasReleased(MappedInputManager::Button::Right); 85 - 86 - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; 87 77 const int pageItems = getPageItems(); 78 + const int totalItems = static_cast<int>(xtc->getChapters().size()); 88 79 89 80 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 90 81 const auto& chapters = xtc->getChapters(); ··· 93 84 } 94 85 } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { 95 86 onGoBack(); 96 - } else if (prevReleased) { 97 - const int total = static_cast<int>(xtc->getChapters().size()); 98 - if (total == 0) { 99 - return; 100 - } 101 - if (skipPage) { 102 - selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + total) % total; 103 - } else { 104 - selectorIndex = (selectorIndex + total - 1) % total; 105 - } 87 + } 88 + 89 + buttonNavigator.onNextRelease([this, totalItems] { 90 + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); 91 + updateRequired = true; 92 + }); 93 + 94 + buttonNavigator.onPreviousRelease([this, totalItems] { 95 + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); 96 + updateRequired = true; 97 + }); 98 + 99 + buttonNavigator.onNextContinuous([this, totalItems, pageItems] { 100 + selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems); 106 101 updateRequired = true; 107 - } else if (nextReleased) { 108 - const int total = static_cast<int>(xtc->getChapters().size()); 109 - if (total == 0) { 110 - return; 111 - } 112 - if (skipPage) { 113 - selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % total; 114 - } else { 115 - selectorIndex = (selectorIndex + 1) % total; 116 - } 102 + }); 103 + 104 + buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] { 105 + selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems); 117 106 updateRequired = true; 118 - } 107 + }); 119 108 } 120 109 121 110 void XtcReaderChapterSelectionActivity::displayTaskLoop() {
+2
src/activities/reader/XtcReaderChapterSelectionActivity.h
··· 7 7 #include <memory> 8 8 9 9 #include "../Activity.h" 10 + #include "util/ButtonNavigator.h" 10 11 11 12 class XtcReaderChapterSelectionActivity final : public Activity { 12 13 std::shared_ptr<Xtc> xtc; 13 14 TaskHandle_t displayTaskHandle = nullptr; 14 15 SemaphoreHandle_t renderingMutex = nullptr; 16 + ButtonNavigator buttonNavigator; 15 17 uint32_t currentPage = 0; 16 18 int selectorIndex = 0; 17 19 bool updateRequired = false;
+8 -7
src/activities/settings/CalibreSettingsActivity.cpp
··· 63 63 return; 64 64 } 65 65 66 - if (mappedInput.wasPressed(MappedInputManager::Button::Up) || 67 - mappedInput.wasPressed(MappedInputManager::Button::Left)) { 66 + // Handle navigation 67 + buttonNavigator.onNext([this] { 68 + selectedIndex = (selectedIndex + 1) % MENU_ITEMS; 69 + updateRequired = true; 70 + }); 71 + 72 + buttonNavigator.onPrevious([this] { 68 73 selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; 69 74 updateRequired = true; 70 - } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || 71 - mappedInput.wasPressed(MappedInputManager::Button::Right)) { 72 - selectedIndex = (selectedIndex + 1) % MENU_ITEMS; 73 - updateRequired = true; 74 - } 75 + }); 75 76 } 76 77 77 78 void CalibreSettingsActivity::handleSelection() {
+2
src/activities/settings/CalibreSettingsActivity.h
··· 6 6 #include <functional> 7 7 8 8 #include "activities/ActivityWithSubactivity.h" 9 + #include "util/ButtonNavigator.h" 9 10 10 11 /** 11 12 * Submenu for OPDS Browser settings. ··· 24 25 private: 25 26 TaskHandle_t displayTaskHandle = nullptr; 26 27 SemaphoreHandle_t renderingMutex = nullptr; 28 + ButtonNavigator buttonNavigator; 27 29 bool updateRequired = false; 28 30 29 31 int selectedIndex = 0;
+8 -7
src/activities/settings/KOReaderSettingsActivity.cpp
··· 64 64 return; 65 65 } 66 66 67 - if (mappedInput.wasPressed(MappedInputManager::Button::Up) || 68 - mappedInput.wasPressed(MappedInputManager::Button::Left)) { 67 + // Handle navigation 68 + buttonNavigator.onNext([this] { 69 + selectedIndex = (selectedIndex + 1) % MENU_ITEMS; 70 + updateRequired = true; 71 + }); 72 + 73 + buttonNavigator.onPrevious([this] { 69 74 selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; 70 75 updateRequired = true; 71 - } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || 72 - mappedInput.wasPressed(MappedInputManager::Button::Right)) { 73 - selectedIndex = (selectedIndex + 1) % MENU_ITEMS; 74 - updateRequired = true; 75 - } 76 + }); 76 77 } 77 78 78 79 void KOReaderSettingsActivity::handleSelection() {
+2
src/activities/settings/KOReaderSettingsActivity.h
··· 6 6 #include <functional> 7 7 8 8 #include "activities/ActivityWithSubactivity.h" 9 + #include "util/ButtonNavigator.h" 9 10 10 11 /** 11 12 * Submenu for KOReader Sync settings. ··· 24 25 private: 25 26 TaskHandle_t displayTaskHandle = nullptr; 26 27 SemaphoreHandle_t renderingMutex = nullptr; 28 + ButtonNavigator buttonNavigator; 27 29 bool updateRequired = false; 28 30 29 31 int selectedIndex = 0;
+17 -21
src/activities/settings/SettingsActivity.cpp
··· 16 16 17 17 const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"}; 18 18 19 - namespace { 20 - constexpr int changeTabsMs = 700; 21 - } // namespace 22 - 23 19 void SettingsActivity::taskTrampoline(void* param) { 24 20 auto* self = static_cast<SettingsActivity*>(param); 25 21 self->displayTaskLoop(); ··· 116 112 return; 117 113 } 118 114 119 - const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up); 120 - const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down); 121 - const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left); 122 - const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right); 123 - const bool changeTab = mappedInput.getHeldTime() > changeTabsMs; 124 - 125 115 // Handle navigation 126 - if (upReleased && changeTab) { 127 - hasChangedCategory = true; 128 - selectedCategoryIndex = (selectedCategoryIndex > 0) ? (selectedCategoryIndex - 1) : (categoryCount - 1); 116 + buttonNavigator.onNextRelease([this] { 117 + selectedSettingIndex = ButtonNavigator::nextIndex(selectedSettingIndex, settingsCount + 1); 129 118 updateRequired = true; 130 - } else if (downReleased && changeTab) { 131 - hasChangedCategory = true; 132 - selectedCategoryIndex = (selectedCategoryIndex < categoryCount - 1) ? (selectedCategoryIndex + 1) : 0; 119 + }); 120 + 121 + buttonNavigator.onPreviousRelease([this] { 122 + selectedSettingIndex = ButtonNavigator::previousIndex(selectedSettingIndex, settingsCount + 1); 133 123 updateRequired = true; 134 - } else if (upReleased || leftReleased) { 135 - selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount); 124 + }); 125 + 126 + buttonNavigator.onNextContinuous([this, &hasChangedCategory] { 127 + hasChangedCategory = true; 128 + selectedCategoryIndex = ButtonNavigator::nextIndex(selectedCategoryIndex, categoryCount); 136 129 updateRequired = true; 137 - } else if (rightReleased || downReleased) { 138 - selectedSettingIndex = (selectedSettingIndex < settingsCount) ? (selectedSettingIndex + 1) : 0; 130 + }); 131 + 132 + buttonNavigator.onPreviousContinuous([this, &hasChangedCategory] { 133 + hasChangedCategory = true; 134 + selectedCategoryIndex = ButtonNavigator::previousIndex(selectedCategoryIndex, categoryCount); 139 135 updateRequired = true; 140 - } 136 + }); 141 137 142 138 if (hasChangedCategory) { 143 139 selectedSettingIndex = (selectedSettingIndex == 0) ? 0 : 1;
+2
src/activities/settings/SettingsActivity.h
··· 8 8 #include <vector> 9 9 10 10 #include "activities/ActivityWithSubactivity.h" 11 + #include "util/ButtonNavigator.h" 11 12 12 13 class CrossPointSettings; 13 14 ··· 124 125 class SettingsActivity final : public ActivityWithSubactivity { 125 126 TaskHandle_t displayTaskHandle = nullptr; 126 127 SemaphoreHandle_t renderingMutex = nullptr; 128 + ButtonNavigator buttonNavigator; 127 129 bool updateRequired = false; 128 130 int selectedCategoryIndex = 0; // Currently selected category 129 131 int selectedSettingIndex = 0;
+20 -46
src/activities/util/KeyboardEntryActivity.cpp
··· 142 142 } 143 143 144 144 void KeyboardEntryActivity::loop() { 145 - // Navigation 146 - if (mappedInput.wasPressed(MappedInputManager::Button::Up)) { 147 - if (selectedRow > 0) { 148 - selectedRow--; 149 - // Clamp column to valid range for new row 150 - const int maxCol = getRowLength(selectedRow) - 1; 151 - if (selectedCol > maxCol) selectedCol = maxCol; 152 - } else { 153 - // Wrap to bottom row 154 - selectedRow = NUM_ROWS - 1; 155 - const int maxCol = getRowLength(selectedRow) - 1; 156 - if (selectedCol > maxCol) selectedCol = maxCol; 157 - } 145 + // Handle navigation 146 + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { 147 + selectedRow = ButtonNavigator::previousIndex(selectedRow, NUM_ROWS); 148 + 149 + const int maxCol = getRowLength(selectedRow) - 1; 150 + if (selectedCol > maxCol) selectedCol = maxCol; 158 151 updateRequired = true; 159 - } 152 + }); 160 153 161 - if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { 162 - if (selectedRow < NUM_ROWS - 1) { 163 - selectedRow++; 164 - const int maxCol = getRowLength(selectedRow) - 1; 165 - if (selectedCol > maxCol) selectedCol = maxCol; 166 - } else { 167 - // Wrap to top row 168 - selectedRow = 0; 169 - const int maxCol = getRowLength(selectedRow) - 1; 170 - if (selectedCol > maxCol) selectedCol = maxCol; 171 - } 154 + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { 155 + selectedRow = ButtonNavigator::nextIndex(selectedRow, NUM_ROWS); 156 + 157 + const int maxCol = getRowLength(selectedRow) - 1; 158 + if (selectedCol > maxCol) selectedCol = maxCol; 172 159 updateRequired = true; 173 - } 160 + }); 174 161 175 - if (mappedInput.wasPressed(MappedInputManager::Button::Left)) { 162 + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { 176 163 const int maxCol = getRowLength(selectedRow) - 1; 177 164 178 165 // Special bottom row case ··· 191 178 // At done button, move to backspace 192 179 selectedCol = BACKSPACE_COL; 193 180 } 194 - updateRequired = true; 195 - return; 181 + } else { 182 + selectedCol = ButtonNavigator::previousIndex(selectedCol, maxCol + 1); 196 183 } 197 184 198 - if (selectedCol > 0) { 199 - selectedCol--; 200 - } else { 201 - // Wrap to end of current row 202 - selectedCol = maxCol; 203 - } 204 185 updateRequired = true; 205 - } 186 + }); 206 187 207 - if (mappedInput.wasPressed(MappedInputManager::Button::Right)) { 188 + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { 208 189 const int maxCol = getRowLength(selectedRow) - 1; 209 190 210 191 // Special bottom row case ··· 223 204 // At done button, wrap to beginning of row 224 205 selectedCol = SHIFT_COL; 225 206 } 226 - updateRequired = true; 227 - return; 228 - } 229 - 230 - if (selectedCol < maxCol) { 231 - selectedCol++; 232 207 } else { 233 - // Wrap to beginning of current row 234 - selectedCol = 0; 208 + selectedCol = ButtonNavigator::nextIndex(selectedCol, maxCol + 1); 235 209 } 236 210 updateRequired = true; 237 - } 211 + }); 238 212 239 213 // Selection 240 214 if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
+2
src/activities/util/KeyboardEntryActivity.h
··· 9 9 #include <utility> 10 10 11 11 #include "../Activity.h" 12 + #include "util/ButtonNavigator.h" 12 13 13 14 /** 14 15 * Reusable keyboard entry activity for text input. ··· 65 66 bool isPassword; 66 67 TaskHandle_t displayTaskHandle = nullptr; 67 68 SemaphoreHandle_t renderingMutex = nullptr; 69 + ButtonNavigator buttonNavigator; 68 70 bool updateRequired = false; 69 71 70 72 // Keyboard state
+2
src/main.cpp
··· 27 27 #include "activities/util/FullScreenMessageActivity.h" 28 28 #include "components/UITheme.h" 29 29 #include "fontIds.h" 30 + #include "util/ButtonNavigator.h" 30 31 31 32 HalDisplay display; 32 33 HalGPIO gpio; ··· 304 305 SETTINGS.loadFromFile(); 305 306 KOREADER_STORE.loadFromFile(); 306 307 UITheme::getInstance().reload(); 308 + ButtonNavigator::setMappedInputManager(mappedInputManager); 307 309 308 310 switch (gpio.getWakeupReason()) { 309 311 case HalGPIO::WakeupReason::PowerButton:
+124
src/util/ButtonNavigator.cpp
··· 1 + #include "ButtonNavigator.h" 2 + 3 + const MappedInputManager* ButtonNavigator::mappedInput = nullptr; 4 + 5 + void ButtonNavigator::onNext(const Callback& callback) { 6 + onNextPress(callback); 7 + onNextContinuous(callback); 8 + } 9 + 10 + void ButtonNavigator::onPrevious(const Callback& callback) { 11 + onPreviousPress(callback); 12 + onPreviousContinuous(callback); 13 + } 14 + 15 + void ButtonNavigator::onPressAndContinuous(const Buttons& buttons, const Callback& callback) { 16 + onPress(buttons, callback); 17 + onContinuous(buttons, callback); 18 + } 19 + 20 + void ButtonNavigator::onNextPress(const Callback& callback) { onPress(getNextButtons(), callback); } 21 + 22 + void ButtonNavigator::onPreviousPress(const Callback& callback) { onPress(getPreviousButtons(), callback); } 23 + 24 + void ButtonNavigator::onNextRelease(const Callback& callback) { onRelease(getNextButtons(), callback); } 25 + 26 + void ButtonNavigator::onPreviousRelease(const Callback& callback) { onRelease(getPreviousButtons(), callback); } 27 + 28 + void ButtonNavigator::onNextContinuous(const Callback& callback) { onContinuous(getNextButtons(), callback); } 29 + 30 + void ButtonNavigator::onPreviousContinuous(const Callback& callback) { onContinuous(getPreviousButtons(), callback); } 31 + 32 + void ButtonNavigator::onPress(const Buttons& buttons, const Callback& callback) { 33 + const bool wasPressed = std::any_of(buttons.begin(), buttons.end(), [](const MappedInputManager::Button button) { 34 + return mappedInput != nullptr && mappedInput->wasPressed(button); 35 + }); 36 + 37 + if (wasPressed) { 38 + callback(); 39 + } 40 + } 41 + 42 + void ButtonNavigator::onRelease(const Buttons& buttons, const Callback& callback) { 43 + const bool wasReleased = std::any_of(buttons.begin(), buttons.end(), [](const MappedInputManager::Button button) { 44 + return mappedInput != nullptr && mappedInput->wasReleased(button); 45 + }); 46 + 47 + if (wasReleased) { 48 + if (lastContinuousNavTime == 0) { 49 + callback(); 50 + } 51 + 52 + lastContinuousNavTime = 0; 53 + } 54 + } 55 + 56 + void ButtonNavigator::onContinuous(const Buttons& buttons, const Callback& callback) { 57 + const bool isPressed = std::any_of(buttons.begin(), buttons.end(), [this](const MappedInputManager::Button button) { 58 + return mappedInput != nullptr && mappedInput->isPressed(button) && shouldNavigateContinuously(); 59 + }); 60 + 61 + if (isPressed) { 62 + callback(); 63 + lastContinuousNavTime = millis(); 64 + } 65 + } 66 + 67 + bool ButtonNavigator::shouldNavigateContinuously() const { 68 + if (!mappedInput) return false; 69 + 70 + const bool buttonHeldLongEnough = mappedInput->getHeldTime() > continuousStartMs; 71 + const bool navigationIntervalElapsed = (millis() - lastContinuousNavTime) > continuousIntervalMs; 72 + 73 + return buttonHeldLongEnough && navigationIntervalElapsed; 74 + } 75 + 76 + int ButtonNavigator::nextIndex(const int currentIndex, const int totalItems) { 77 + if (totalItems <= 0) return 0; 78 + 79 + // Calculate the next index with wrap-around 80 + return (currentIndex + 1) % totalItems; 81 + } 82 + 83 + int ButtonNavigator::previousIndex(const int currentIndex, const int totalItems) { 84 + if (totalItems <= 0) return 0; 85 + 86 + // Calculate the previous index with wrap-around 87 + return (currentIndex + totalItems - 1) % totalItems; 88 + } 89 + 90 + int ButtonNavigator::nextPageIndex(const int currentIndex, const int totalItems, const int itemsPerPage) { 91 + if (totalItems <= 0 || itemsPerPage <= 0) return 0; 92 + 93 + // When items fit on one page, use index navigation instead 94 + if (totalItems <= itemsPerPage) { 95 + return nextIndex(currentIndex, totalItems); 96 + } 97 + 98 + const int lastPageIndex = (totalItems - 1) / itemsPerPage; 99 + const int currentPageIndex = currentIndex / itemsPerPage; 100 + 101 + if (currentPageIndex < lastPageIndex) { 102 + return (currentPageIndex + 1) * itemsPerPage; 103 + } 104 + 105 + return 0; 106 + } 107 + 108 + int ButtonNavigator::previousPageIndex(const int currentIndex, const int totalItems, const int itemsPerPage) { 109 + if (totalItems <= 0 || itemsPerPage <= 0) return 0; 110 + 111 + // When items fit on one page, use index navigation instead 112 + if (totalItems <= itemsPerPage) { 113 + return previousIndex(currentIndex, totalItems); 114 + } 115 + 116 + const int lastPageIndex = (totalItems - 1) / itemsPerPage; 117 + const int currentPageIndex = currentIndex / itemsPerPage; 118 + 119 + if (currentPageIndex > 0) { 120 + return (currentPageIndex - 1) * itemsPerPage; 121 + } 122 + 123 + return lastPageIndex * itemsPerPage; 124 + }
+53
src/util/ButtonNavigator.h
··· 1 + #pragma once 2 + 3 + #include <functional> 4 + #include <vector> 5 + 6 + #include "MappedInputManager.h" 7 + 8 + class ButtonNavigator final { 9 + using Callback = std::function<void()>; 10 + using Buttons = std::vector<MappedInputManager::Button>; 11 + 12 + const uint16_t continuousStartMs; 13 + const uint16_t continuousIntervalMs; 14 + uint32_t lastContinuousNavTime = 0; 15 + static const MappedInputManager* mappedInput; 16 + 17 + [[nodiscard]] bool shouldNavigateContinuously() const; 18 + 19 + public: 20 + explicit ButtonNavigator(const uint16_t continuousIntervalMs = 500, const uint16_t continuousStartMs = 500) 21 + : continuousStartMs(continuousStartMs), continuousIntervalMs(continuousIntervalMs) {} 22 + 23 + static void setMappedInputManager(const MappedInputManager& mappedInputManager) { mappedInput = &mappedInputManager; } 24 + 25 + void onNext(const Callback& callback); 26 + void onPrevious(const Callback& callback); 27 + void onPressAndContinuous(const Buttons& buttons, const Callback& callback); 28 + 29 + void onNextPress(const Callback& callback); 30 + void onPreviousPress(const Callback& callback); 31 + void onPress(const Buttons& buttons, const Callback& callback); 32 + 33 + void onNextRelease(const Callback& callback); 34 + void onPreviousRelease(const Callback& callback); 35 + void onRelease(const Buttons& buttons, const Callback& callback); 36 + 37 + void onNextContinuous(const Callback& callback); 38 + void onPreviousContinuous(const Callback& callback); 39 + void onContinuous(const Buttons& buttons, const Callback& callback); 40 + 41 + [[nodiscard]] static int nextIndex(int currentIndex, int totalItems); 42 + [[nodiscard]] static int previousIndex(int currentIndex, int totalItems); 43 + 44 + [[nodiscard]] static int nextPageIndex(int currentIndex, int totalItems, int itemsPerPage); 45 + [[nodiscard]] static int previousPageIndex(int currentIndex, int totalItems, int itemsPerPage); 46 + 47 + [[nodiscard]] static Buttons getNextButtons() { 48 + return {MappedInputManager::Button::Down, MappedInputManager::Button::Right}; 49 + } 50 + [[nodiscard]] static Buttons getPreviousButtons() { 51 + return {MappedInputManager::Button::Up, MappedInputManager::Button::Left}; 52 + } 53 + };