diff --git a/src/ScreenComponents.cpp b/src/ScreenComponents.cpp index f414ca06..2e8d9e7c 100644 --- a/src/ScreenComponents.cpp +++ b/src/ScreenComponents.cpp @@ -8,12 +8,11 @@ #include "Battery.h" #include "fontIds.h" -void ScreenComponents::drawBattery(const GfxRenderer &renderer, const int left, - const int top, const bool showPercentage) { +void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left, const int top, + const bool showPercentage) { // Left aligned battery icon and percentage const uint16_t percentage = battery.readPercentage(); - const auto percentageText = - showPercentage ? std::to_string(percentage) + "%" : ""; + const auto percentageText = showPercentage ? std::to_string(percentage) + "%" : ""; renderer.drawText(SMALL_FONT_ID, left + 20, top, percentageText.c_str()); // 1 column on left, 2 columns on right, 5 columns of battery body @@ -25,53 +24,46 @@ void ScreenComponents::drawBattery(const GfxRenderer &renderer, const int left, // Top line renderer.drawLine(x + 1, y, x + batteryWidth - 3, y); // Bottom line - renderer.drawLine(x + 1, y + batteryHeight - 1, x + batteryWidth - 3, - y + batteryHeight - 1); + renderer.drawLine(x + 1, y + batteryHeight - 1, x + batteryWidth - 3, y + batteryHeight - 1); // Left line renderer.drawLine(x, y + 1, x, y + batteryHeight - 2); // Battery end - renderer.drawLine(x + batteryWidth - 2, y + 1, x + batteryWidth - 2, - y + batteryHeight - 2); + renderer.drawLine(x + batteryWidth - 2, y + 1, x + batteryWidth - 2, y + batteryHeight - 2); renderer.drawPixel(x + batteryWidth - 1, y + 3); renderer.drawPixel(x + batteryWidth - 1, y + batteryHeight - 4); - renderer.drawLine(x + batteryWidth - 0, y + 4, x + batteryWidth - 0, - y + batteryHeight - 5); + renderer.drawLine(x + batteryWidth - 0, y + 4, x + batteryWidth - 0, y + batteryHeight - 5); // The +1 is to round up, so that we always fill at least one pixel int filledWidth = percentage * (batteryWidth - 5) / 100 + 1; if (filledWidth > batteryWidth - 5) { - filledWidth = batteryWidth - 5; // Ensure we don't overflow + filledWidth = batteryWidth - 5; // Ensure we don't overflow } renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4); } -int ScreenComponents::drawTabBar(const GfxRenderer &renderer, const int y, - const std::vector &tabs) { - constexpr int tabPadding = 20; // Horizontal padding between tabs - constexpr int leftMargin = 20; // Left margin for first tab - constexpr int underlineHeight = 2; // Height of selection underline - constexpr int underlineGap = 4; // Gap between text and underline +int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector& tabs) { + constexpr int tabPadding = 20; // Horizontal padding between tabs + constexpr int leftMargin = 20; // Left margin for first tab + constexpr int underlineHeight = 2; // Height of selection underline + constexpr int underlineGap = 4; // Gap between text and underline const int lineHeight = renderer.getLineHeight(UI_12_FONT_ID); const int tabBarHeight = lineHeight + underlineGap + underlineHeight; int currentX = leftMargin; - for (const auto &tab : tabs) { - const int textWidth = renderer.getTextWidth( - UI_12_FONT_ID, tab.label, - tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); + for (const auto& tab : tabs) { + const int textWidth = + renderer.getTextWidth(UI_12_FONT_ID, tab.label, tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); // Draw tab label renderer.drawText(UI_12_FONT_ID, currentX, y, tab.label, true, - tab.selected ? EpdFontFamily::BOLD - : EpdFontFamily::REGULAR); + tab.selected ? EpdFontFamily::BOLD : EpdFontFamily::REGULAR); // Draw underline for selected tab if (tab.selected) { - renderer.fillRect(currentX, y + lineHeight + underlineGap, textWidth, - underlineHeight); + renderer.fillRect(currentX, y + lineHeight + underlineGap, textWidth, underlineHeight); } currentX += textWidth + tabPadding; @@ -80,64 +72,53 @@ int ScreenComponents::drawTabBar(const GfxRenderer &renderer, const int y, return tabBarHeight; } -void ScreenComponents::drawScrollIndicator(const GfxRenderer &renderer, - const int currentPage, - const int totalPages, - const int contentTop, - const int contentHeight) { +void ScreenComponents::drawScrollIndicator(const GfxRenderer& renderer, const int currentPage, const int totalPages, + const int contentTop, const int contentHeight) { if (totalPages <= 1) { - return; // No need for indicator if only one page + return; // No need for indicator if only one page } const int screenWidth = renderer.getScreenWidth(); constexpr int indicatorWidth = 20; constexpr int arrowSize = 6; - constexpr int margin = 15; // Offset from right edge + constexpr int margin = 15; // Offset from right edge const int centerX = screenWidth - indicatorWidth / 2 - margin; - const int indicatorTop = - contentTop + 60; // Offset to avoid overlapping side button hints + const int indicatorTop = contentTop + 60; // Offset to avoid overlapping side button hints const int indicatorBottom = contentTop + contentHeight - 30; // Draw up arrow at top (^) - narrow point at top, wide base at bottom for (int i = 0; i < arrowSize; ++i) { const int lineWidth = 1 + i * 2; const int startX = centerX - i; - renderer.drawLine(startX, indicatorTop + i, startX + lineWidth - 1, - indicatorTop + i); + renderer.drawLine(startX, indicatorTop + i, startX + lineWidth - 1, indicatorTop + i); } // Draw down arrow at bottom (v) - wide base at top, narrow point at bottom for (int i = 0; i < arrowSize; ++i) { const int lineWidth = 1 + (arrowSize - 1 - i) * 2; const int startX = centerX - (arrowSize - 1 - i); - renderer.drawLine(startX, indicatorBottom - arrowSize + 1 + i, - startX + lineWidth - 1, + renderer.drawLine(startX, indicatorBottom - arrowSize + 1 + i, startX + lineWidth - 1, indicatorBottom - arrowSize + 1 + i); } // Draw page fraction in the middle (e.g., "1/3") - const std::string pageText = - std::to_string(currentPage) + "/" + std::to_string(totalPages); + const std::string pageText = std::to_string(currentPage) + "/" + std::to_string(totalPages); const int textWidth = renderer.getTextWidth(SMALL_FONT_ID, pageText.c_str()); const int textX = centerX - textWidth / 2; - const int textY = (indicatorTop + indicatorBottom) / 2 - - renderer.getLineHeight(SMALL_FONT_ID) / 2; + const int textY = (indicatorTop + indicatorBottom) / 2 - renderer.getLineHeight(SMALL_FONT_ID) / 2; renderer.drawText(SMALL_FONT_ID, textX, textY, pageText.c_str()); } -void ScreenComponents::drawProgressBar(const GfxRenderer &renderer, const int x, - const int y, const int width, - const int height, const size_t current, - const size_t total) { +void ScreenComponents::drawProgressBar(const GfxRenderer& renderer, const int x, const int y, const int width, + const int height, const size_t current, const size_t total) { if (total == 0) { return; } // Use 64-bit arithmetic to avoid overflow for large files - const int percent = - static_cast((static_cast(current) * 100) / total); + const int percent = static_cast((static_cast(current) * 100) / total); // Draw outline renderer.drawRect(x, y, width, height); @@ -150,6 +131,5 @@ void ScreenComponents::drawProgressBar(const GfxRenderer &renderer, const int x, // Draw percentage text centered below bar const std::string percentText = std::to_string(percent) + "%"; - renderer.drawCenteredText(UI_10_FONT_ID, y + height + 15, - percentText.c_str()); + renderer.drawCenteredText(UI_10_FONT_ID, y + height + 15, percentText.c_str()); } diff --git a/src/ScreenComponents.h b/src/ScreenComponents.h index 7ebb43f8..48c40f42 100644 --- a/src/ScreenComponents.h +++ b/src/ScreenComponents.h @@ -7,24 +7,21 @@ class GfxRenderer; struct TabInfo { - const char *label; + const char* label; bool selected; }; class ScreenComponents { -public: - static void drawBattery(const GfxRenderer &renderer, int left, int top, - bool showPercentage = true); + public: + static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true); // Draw a horizontal tab bar with underline indicator for selected tab // Returns the height of the tab bar (for positioning content below) - static int drawTabBar(const GfxRenderer &renderer, int y, - const std::vector &tabs); + static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector& tabs); // Draw a scroll/page indicator on the right side of the screen // Shows up/down arrows and current page fraction (e.g., "1/3") - static void drawScrollIndicator(const GfxRenderer &renderer, int currentPage, - int totalPages, int contentTop, + static void drawScrollIndicator(const GfxRenderer& renderer, int currentPage, int totalPages, int contentTop, int contentHeight); /** @@ -37,7 +34,6 @@ public: * @param current Current progress value * @param total Total value for 100% progress */ - static void drawProgressBar(const GfxRenderer &renderer, int x, int y, - int width, int height, size_t current, + static void drawProgressBar(const GfxRenderer& renderer, int x, int y, int width, int height, size_t current, size_t total); }; diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 01b34a4f..60b7ad35 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -16,31 +16,26 @@ constexpr int TAB_BAR_Y = 15; constexpr int CONTENT_START_Y = 60; constexpr int LINE_HEIGHT = 30; constexpr int LEFT_MARGIN = 20; -constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator +constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator // Timing thresholds constexpr int SKIP_PAGE_MS = 700; constexpr unsigned long GO_HOME_MS = 1000; -void sortFileList(std::vector &strs) { - std::sort(begin(strs), end(strs), - [](const std::string &str1, const std::string &str2) { - if (str1.back() == '/' && str2.back() != '/') - return true; - if (str1.back() != '/' && str2.back() == '/') - return false; - return lexicographical_compare( - begin(str1), end(str1), begin(str2), end(str2), - [](const char &char1, const char &char2) { - return tolower(char1) < tolower(char2); - }); - }); +void sortFileList(std::vector& strs) { + std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { + if (str1.back() == '/' && str2.back() != '/') return true; + if (str1.back() != '/' && str2.back() == '/') return false; + return lexicographical_compare( + begin(str1), end(str1), begin(str2), end(str2), + [](const char& char1, const char& char2) { return tolower(char1) < tolower(char2); }); + }); } -} // namespace +} // namespace int MyLibraryActivity::getPageItems() const { const int screenHeight = renderer.getScreenHeight(); - const int bottomBarHeight = 60; // Space for button hints + const int bottomBarHeight = 60; // Space for button hints const int availableHeight = screenHeight - CONTENT_START_Y - bottomBarHeight; int items = availableHeight / LINE_HEIGHT; if (items < 1) { @@ -59,8 +54,7 @@ int MyLibraryActivity::getCurrentItemCount() const { int MyLibraryActivity::getTotalPages() const { const int itemCount = getCurrentItemCount(); const int pageItems = getPageItems(); - if (itemCount == 0) - return 1; + if (itemCount == 0) return 1; return (itemCount + pageItems - 1) / pageItems; } @@ -74,11 +68,11 @@ void MyLibraryActivity::loadRecentBooks() { bookTitles.clear(); bookPaths.clear(); - const auto &books = RECENT_BOOKS.getBooks(); + const auto& books = RECENT_BOOKS.getBooks(); bookTitles.reserve(std::min(books.size(), MAX_RECENT_BOOKS)); bookPaths.reserve(std::min(books.size(), MAX_RECENT_BOOKS)); - for (const auto &path : books) { + for (const auto& path : books) { // Limit to maximum number of recent books if (bookTitles.size() >= MAX_RECENT_BOOKS) { break; @@ -106,8 +100,7 @@ void MyLibraryActivity::loadFiles() { auto root = SdMan.open(basepath.c_str()); if (!root || !root.isDirectory()) { - if (root) - root.close(); + if (root) root.close(); return; } @@ -125,10 +118,8 @@ void MyLibraryActivity::loadFiles() { files.emplace_back(std::string(name) + "/"); } else { auto filename = std::string(name); - std::string ext4 = - filename.length() >= 4 ? filename.substr(filename.length() - 4) : ""; - std::string ext5 = - filename.length() >= 5 ? filename.substr(filename.length() - 5) : ""; + std::string ext4 = filename.length() >= 4 ? filename.substr(filename.length() - 4) : ""; + std::string ext5 = filename.length() >= 5 ? filename.substr(filename.length() - 5) : ""; if (ext5 == ".epub" || ext5 == ".xtch" || ext4 == ".xtc") { files.emplace_back(filename); } @@ -139,8 +130,8 @@ void MyLibraryActivity::loadFiles() { sortFileList(files); } -void MyLibraryActivity::taskTrampoline(void *param) { - auto *self = static_cast(param); +void MyLibraryActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); self->displayTaskLoop(); } @@ -157,10 +148,10 @@ void MyLibraryActivity::onEnter() { updateRequired = true; xTaskCreate(&MyLibraryActivity::taskTrampoline, "MyLibraryActivityTask", - 4096, // Stack size (increased for epub metadata loading) - this, // Parameters - 1, // Priority - &displayTaskHandle // Task handle + 4096, // Stack size (increased for epub metadata loading) + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle ); } @@ -187,8 +178,7 @@ void MyLibraryActivity::loop() { const int pageItems = getPageItems(); // Long press BACK (1s+) in Files tab goes to root folder - if (currentTab == Tab::Files && - mappedInput.isPressed(MappedInputManager::Button::Back) && + if (currentTab == Tab::Files && mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS) { if (basepath != "/") { basepath = "/"; @@ -199,33 +189,26 @@ void MyLibraryActivity::loop() { return; } - const bool upReleased = - mappedInput.wasReleased(MappedInputManager::Button::Up); - const bool downReleased = - mappedInput.wasReleased(MappedInputManager::Button::Down); - const bool leftReleased = - mappedInput.wasReleased(MappedInputManager::Button::Left); - const bool rightReleased = - mappedInput.wasReleased(MappedInputManager::Button::Right); + const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up); + const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down); + const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left); + const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right); const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; // Confirm button - open selected item if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (currentTab == Tab::Recent) { - if (!bookPaths.empty() && - selectorIndex < static_cast(bookPaths.size())) { + if (!bookPaths.empty() && selectorIndex < static_cast(bookPaths.size())) { onSelectBook(bookPaths[selectorIndex]); } } else { // Files tab if (!files.empty() && selectorIndex < static_cast(files.size())) { - if (basepath.back() != '/') - basepath += "/"; + if (basepath.back() != '/') basepath += "/"; if (files[selectorIndex].back() == '/') { // Enter directory - basepath += - files[selectorIndex].substr(0, files[selectorIndex].length() - 1); + basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1); loadFiles(); selectorIndex = 0; updateRequired = true; @@ -244,8 +227,7 @@ void MyLibraryActivity::loop() { if (currentTab == Tab::Files && basepath != "/") { // Go up one directory basepath.replace(basepath.find_last_of('/'), std::string::npos, ""); - if (basepath.empty()) - basepath = "/"; + if (basepath.empty()) basepath = "/"; loadFiles(); selectorIndex = 0; updateRequired = true; @@ -277,8 +259,7 @@ void MyLibraryActivity::loop() { if (prevReleased && itemCount > 0) { if (skipPage) { - selectorIndex = - ((selectorIndex / pageItems - 1) * pageItems + itemCount) % itemCount; + selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + itemCount) % itemCount; } else { selectorIndex = (selectorIndex + itemCount - 1) % itemCount; } @@ -309,8 +290,7 @@ void MyLibraryActivity::render() const { renderer.clearScreen(); // Draw tab bar - std::vector tabs = {{"Recent", currentTab == Tab::Recent}, - {"Files", currentTab == Tab::Files}}; + std::vector tabs = {{"Recent", currentTab == Tab::Recent}, {"Files", currentTab == Tab::Files}}; ScreenComponents::drawTabBar(renderer, TAB_BAR_Y, tabs); // Draw content based on current tab @@ -322,11 +302,8 @@ void MyLibraryActivity::render() const { // Draw scroll indicator const int screenHeight = renderer.getScreenHeight(); - const int contentHeight = - screenHeight - CONTENT_START_Y - 60; // 60 for bottom bar - ScreenComponents::drawScrollIndicator(renderer, getCurrentPage(), - getTotalPages(), CONTENT_START_Y, - contentHeight); + const int contentHeight = screenHeight - CONTENT_START_Y - 60; // 60 for bottom bar + ScreenComponents::drawScrollIndicator(renderer, getCurrentPage(), getTotalPages(), CONTENT_START_Y, contentHeight); // Draw side button hints (up/down navigation on right side) // Note: text is rotated 90° CW, so ">" appears as "^" and "<" appears as "v" @@ -334,11 +311,9 @@ void MyLibraryActivity::render() const { // Draw bottom button hints // In Files tab, show "BACK" when in subdirectory, "HOME" when at root - const char *backLabel = - (currentTab == Tab::Files && basepath != "/") ? "BACK" : "HOME"; + const char* backLabel = (currentTab == Tab::Files && basepath != "/") ? "BACK" : "HOME"; const auto labels = mappedInput.mapLabels(backLabel, "OPEN", "<", ">"); - renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, - labels.btn4); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); } @@ -349,26 +324,21 @@ void MyLibraryActivity::renderRecentTab() const { const int bookCount = static_cast(bookTitles.size()); if (bookCount == 0) { - renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, - "No recent books"); + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No recent books"); return; } const auto pageStartIndex = selectorIndex / pageItems * pageItems; // Draw selection highlight - renderer.fillRect( - 0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, - pageWidth - RIGHT_MARGIN, LINE_HEIGHT); + renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN, + LINE_HEIGHT); // Draw items - for (int i = pageStartIndex; i < bookCount && i < pageStartIndex + pageItems; - i++) { - auto item = renderer.truncatedText(UI_10_FONT_ID, bookTitles[i].c_str(), - pageWidth - LEFT_MARGIN - RIGHT_MARGIN); - renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, - CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, - item.c_str(), i != selectorIndex); + for (int i = pageStartIndex; i < bookCount && i < pageStartIndex + pageItems; i++) { + auto item = renderer.truncatedText(UI_10_FONT_ID, bookTitles[i].c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(), + i != selectorIndex); } } @@ -378,25 +348,20 @@ void MyLibraryActivity::renderFilesTab() const { const int fileCount = static_cast(files.size()); if (fileCount == 0) { - renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, - "No books found"); + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No books found"); return; } const auto pageStartIndex = selectorIndex / pageItems * pageItems; // Draw selection highlight - renderer.fillRect( - 0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, - pageWidth - RIGHT_MARGIN, LINE_HEIGHT); + renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN, + LINE_HEIGHT); // Draw items - for (int i = pageStartIndex; i < fileCount && i < pageStartIndex + pageItems; - i++) { - auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), - pageWidth - LEFT_MARGIN - RIGHT_MARGIN); - renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, - CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, - item.c_str(), i != selectorIndex); + for (int i = pageStartIndex; i < fileCount && i < pageStartIndex + pageItems; i++) { + auto item = renderer.truncatedText(UI_10_FONT_ID, files[i].c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(), + i != selectorIndex); } } diff --git a/src/activities/home/MyLibraryActivity.h b/src/activities/home/MyLibraryActivity.h index ccea820a..e6de28ca 100644 --- a/src/activities/home/MyLibraryActivity.h +++ b/src/activities/home/MyLibraryActivity.h @@ -10,10 +10,10 @@ #include "../Activity.h" class MyLibraryActivity final : public Activity { -public: + public: enum class Tab { Recent, Files }; -private: + private: TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; @@ -22,9 +22,8 @@ private: bool updateRequired = false; // Recent tab state - std::vector bookTitles; // Display titles for each book - std::vector - bookPaths; // Paths for each visible book (excludes missing) + std::vector bookTitles; // Display titles for each book + std::vector bookPaths; // Paths for each visible book (excludes missing) // Files tab state (from FileSelectionActivity) std::string basepath = "/"; @@ -32,7 +31,7 @@ private: // Callbacks const std::function onGoHome; - const std::function onSelectBook; + const std::function onSelectBook; // Number of items that fit on a page int getPageItems() const; @@ -45,20 +44,21 @@ private: void loadFiles(); // Rendering - static void taskTrampoline(void *param); + static void taskTrampoline(void* param); [[noreturn]] void displayTaskLoop(); void render() const; void renderRecentTab() const; void renderFilesTab() const; -public: - explicit MyLibraryActivity( - GfxRenderer &renderer, MappedInputManager &mappedInput, - const std::function &onGoHome, - const std::function &onSelectBook, - Tab initialTab = Tab::Recent) - : Activity("MyLibrary", renderer, mappedInput), currentTab(initialTab), - onGoHome(onGoHome), onSelectBook(onSelectBook) {} + public: + explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onGoHome, + const std::function& onSelectBook, + Tab initialTab = Tab::Recent) + : Activity("MyLibrary", renderer, mappedInput), + currentTab(initialTab), + onGoHome(onGoHome), + onSelectBook(onSelectBook) {} void onEnter() override; void onExit() override; void loop() override;