From 02350c6a9f4b27f5d9ec49dd215c1d9aa8d776c7 Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Sun, 28 Dec 2025 17:57:06 +1000 Subject: [PATCH] Fix underscore on keyboard and standardize activity (#138) ## Summary * Fix underscore on keyboard * Remove special handling of special row characters * Fix navigating between special row items * Standardize keyboard activity to use standard loop * Fix issue with rendering keyboard non-stop Fixes https://github.com/daveallie/crosspoint-reader/issues/131 --- .../network/WifiSelectionActivity.cpp | 75 ++--- src/activities/util/KeyboardEntryActivity.cpp | 269 ++++++++++-------- src/activities/util/KeyboardEntryActivity.h | 85 ++---- 3 files changed, 207 insertions(+), 222 deletions(-) diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 68c6481..bfbfeb9 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -187,11 +187,21 @@ void WifiSelectionActivity::selectNetwork(const int index) { if (selectedRequiresPassword) { // Show password entry state = WifiSelectionState::PASSWORD_ENTRY; - enterNewActivity(new KeyboardEntryActivity(renderer, inputManager, "Enter WiFi Password", - "", // No initial text - 64, // Max password length - false // Show password by default (hard keyboard to use) - )); + enterNewActivity(new KeyboardEntryActivity( + renderer, inputManager, "Enter WiFi Password", + "", // No initial text + 50, // Y position + 64, // Max password length + false, // Show password by default (hard keyboard to use) + [this](const std::string& text) { + enteredPassword = text; + exitActivity(); + }, + [this] { + state = WifiSelectionState::NETWORK_LIST; + updateRequired = true; + exitActivity(); + })); updateRequired = true; } else { // Connect directly for open networks @@ -208,11 +218,6 @@ void WifiSelectionActivity::attemptConnection() { WiFi.mode(WIFI_STA); - // Get password from keyboard if we just entered it - if (subActivity && !usedSavedPassword) { - enteredPassword = static_cast(subActivity.get())->getText(); - } - if (selectedRequiresPassword && !enteredPassword.empty()) { WiFi.begin(selectedSSID.c_str(), enteredPassword.c_str()); } else { @@ -269,6 +274,11 @@ void WifiSelectionActivity::checkConnectionStatus() { } void WifiSelectionActivity::loop() { + if (subActivity) { + subActivity->loop(); + return; + } + // Check scan progress if (state == WifiSelectionState::SCANNING) { processWifiScanResults(); @@ -281,24 +291,9 @@ void WifiSelectionActivity::loop() { return; } - // Handle password entry state - if (state == WifiSelectionState::PASSWORD_ENTRY && subActivity) { - const auto keyboard = static_cast(subActivity.get()); - keyboard->handleInput(); - - if (keyboard->isComplete()) { - attemptConnection(); - return; - } - - if (keyboard->isCancelled()) { - state = WifiSelectionState::NETWORK_LIST; - exitActivity(); - updateRequired = true; - return; - } - - updateRequired = true; + if (state == WifiSelectionState::PASSWORD_ENTRY) { + // Reach here once password entry finished in subactivity + attemptConnection(); return; } @@ -441,6 +436,10 @@ std::string WifiSelectionActivity::getSignalStrengthIndicator(const int32_t rssi void WifiSelectionActivity::displayTaskLoop() { while (true) { + if (subActivity) { + return; + } + if (updateRequired) { updateRequired = false; xSemaphoreTake(renderingMutex, portMAX_DELAY); @@ -461,9 +460,6 @@ void WifiSelectionActivity::render() const { case WifiSelectionState::NETWORK_LIST: renderNetworkList(); break; - case WifiSelectionState::PASSWORD_ENTRY: - renderPasswordEntry(); - break; case WifiSelectionState::CONNECTING: renderConnecting(); break; @@ -561,23 +557,6 @@ void WifiSelectionActivity::renderNetworkList() const { renderer.drawButtonHints(UI_FONT_ID, "« Back", "Connect", "", ""); } -void WifiSelectionActivity::renderPasswordEntry() const { - // Draw header - renderer.drawCenteredText(READER_FONT_ID, 5, "WiFi Password", true, BOLD); - - // Draw network name with good spacing from header - std::string networkInfo = "Network: " + selectedSSID; - if (networkInfo.length() > 30) { - networkInfo.replace(27, networkInfo.length() - 27, "..."); - } - renderer.drawCenteredText(UI_FONT_ID, 38, networkInfo.c_str(), true, REGULAR); - - // Draw keyboard - if (subActivity) { - static_cast(subActivity.get())->render(58); - } -} - void WifiSelectionActivity::renderConnecting() const { const auto pageHeight = renderer.getScreenHeight(); const auto height = renderer.getLineHeight(UI_FONT_ID); diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index b4ed01c..73f3dde 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -10,41 +10,55 @@ const char* const KeyboardEntryActivity::keyboard[NUM_ROWS] = { // Keyboard layouts - uppercase/symbols const char* const KeyboardEntryActivity::keyboardShift[NUM_ROWS] = {"~!@#$%^&*()_+", "QWERTYUIOP{}|", "ASDFGHJKL:\"", - "ZXCVBNM<>?", "^ _____?", "SPECIAL ROW"}; -void KeyboardEntryActivity::setText(const std::string& newText) { - text = newText; - if (maxLength > 0 && text.length() > maxLength) { - text.resize(maxLength); - } +void KeyboardEntryActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); } -void KeyboardEntryActivity::reset(const std::string& newTitle, const std::string& newInitialText) { - if (!newTitle.empty()) { - title = newTitle; +void KeyboardEntryActivity::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); } - text = newInitialText; - selectedRow = 0; - selectedCol = 0; - shiftActive = false; - complete = false; - cancelled = false; } void KeyboardEntryActivity::onEnter() { Activity::onEnter(); - // Reset state when entering the activity - complete = false; - cancelled = false; + renderingMutex = xSemaphoreCreateMutex(); + + // Trigger first update + updateRequired = true; + + xTaskCreate(&KeyboardEntryActivity::taskTrampoline, "KeyboardEntryActivity", + 2048, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); } -void KeyboardEntryActivity::loop() { - handleInput(); - render(10); +void KeyboardEntryActivity::onExit() { + Activity::onExit(); + + // Wait until not rendering to delete task to avoid killing mid-instruction to EPD + xSemaphoreTake(renderingMutex, portMAX_DELAY); + if (displayTaskHandle) { + vTaskDelete(displayTaskHandle); + displayTaskHandle = nullptr; + } + vSemaphoreDelete(renderingMutex); + renderingMutex = nullptr; } -int KeyboardEntryActivity::getRowLength(int row) const { +int KeyboardEntryActivity::getRowLength(const int row) const { if (row < 0 || row >= NUM_ROWS) return 0; // Return actual length of each row based on keyboard layout @@ -58,7 +72,7 @@ int KeyboardEntryActivity::getRowLength(int row) const { case 3: return 10; // zxcvbnm,./ case 4: - return 10; // ^, space (5 wide), backspace, OK (2 wide) + return 10; // caps (2 wide), space (5 wide), backspace (2 wide), OK default: return 0; } @@ -75,8 +89,8 @@ char KeyboardEntryActivity::getSelectedChar() const { void KeyboardEntryActivity::handleKeyPress() { // Handle special row (bottom row with shift, space, backspace, done) - if (selectedRow == SHIFT_ROW) { - if (selectedCol == SHIFT_COL) { + if (selectedRow == SPECIAL_ROW) { + if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) { // Shift toggle shiftActive = !shiftActive; return; @@ -90,7 +104,7 @@ void KeyboardEntryActivity::handleKeyPress() { return; } - if (selectedCol == BACKSPACE_COL) { + if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) { // Backspace if (!text.empty()) { text.pop_back(); @@ -100,7 +114,6 @@ void KeyboardEntryActivity::handleKeyPress() { if (selectedCol >= DONE_COL) { // Done button - complete = true; if (onComplete) { onComplete(text); } @@ -109,42 +122,61 @@ void KeyboardEntryActivity::handleKeyPress() { } // Regular character - char c = getSelectedChar(); - if (c != '\0' && c != '^' && c != '_' && c != '<') { - if (maxLength == 0 || text.length() < maxLength) { - text += c; - // Auto-disable shift after typing a letter - if (shiftActive && ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))) { - shiftActive = false; - } + const char c = getSelectedChar(); + if (c == '\0') { + return; + } + + if (maxLength == 0 || text.length() < maxLength) { + text += c; + // Auto-disable shift after typing a letter + if (shiftActive && ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'))) { + shiftActive = false; } } } -bool KeyboardEntryActivity::handleInput() { - if (complete || cancelled) { - return false; - } - - bool handled = false; - +void KeyboardEntryActivity::loop() { // Navigation if (inputManager.wasPressed(InputManager::BTN_UP)) { if (selectedRow > 0) { selectedRow--; // Clamp column to valid range for new row - int maxCol = getRowLength(selectedRow) - 1; + const int maxCol = getRowLength(selectedRow) - 1; if (selectedCol > maxCol) selectedCol = maxCol; } - handled = true; - } else if (inputManager.wasPressed(InputManager::BTN_DOWN)) { + updateRequired = true; + } + + if (inputManager.wasPressed(InputManager::BTN_DOWN)) { if (selectedRow < NUM_ROWS - 1) { selectedRow++; - int maxCol = getRowLength(selectedRow) - 1; + const int maxCol = getRowLength(selectedRow) - 1; if (selectedCol > maxCol) selectedCol = maxCol; } - handled = true; - } else if (inputManager.wasPressed(InputManager::BTN_LEFT)) { + updateRequired = true; + } + + if (inputManager.wasPressed(InputManager::BTN_LEFT)) { + // Special bottom row case + if (selectedRow == SPECIAL_ROW) { + // Bottom row has special key widths + if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) { + // In shift key, do nothing + } else if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) { + // In space bar, move to shift + selectedCol = SHIFT_COL; + } else if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) { + // In backspace, move to space + selectedCol = SPACE_COL; + } else if (selectedCol >= DONE_COL) { + // At done button, move to backspace + selectedCol = BACKSPACE_COL; + } + updateRequired = true; + return; + } + if (selectedCol > 0) { selectedCol--; } else if (selectedRow > 0) { @@ -152,9 +184,31 @@ bool KeyboardEntryActivity::handleInput() { selectedRow--; selectedCol = getRowLength(selectedRow) - 1; } - handled = true; - } else if (inputManager.wasPressed(InputManager::BTN_RIGHT)) { - int maxCol = getRowLength(selectedRow) - 1; + updateRequired = true; + } + + if (inputManager.wasPressed(InputManager::BTN_RIGHT)) { + const int maxCol = getRowLength(selectedRow) - 1; + + // Special bottom row case + if (selectedRow == SPECIAL_ROW) { + // Bottom row has special key widths + if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) { + // In shift key, move to space + selectedCol = SPACE_COL; + } else if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) { + // In space bar, move to backspace + selectedCol = BACKSPACE_COL; + } else if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) { + // In backspace, move to done + selectedCol = DONE_COL; + } else if (selectedCol >= DONE_COL) { + // At done button, do nothing + } + updateRequired = true; + return; + } + if (selectedCol < maxCol) { selectedCol++; } else if (selectedRow < NUM_ROWS - 1) { @@ -162,35 +216,34 @@ bool KeyboardEntryActivity::handleInput() { selectedRow++; selectedCol = 0; } - handled = true; + updateRequired = true; } // Selection if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) { handleKeyPress(); - handled = true; + updateRequired = true; } // Cancel if (inputManager.wasPressed(InputManager::BTN_BACK)) { - cancelled = true; if (onCancel) { onCancel(); } - handled = true; + updateRequired = true; } - - return handled; } -void KeyboardEntryActivity::render(int startY) const { +void KeyboardEntryActivity::render() const { const auto pageWidth = GfxRenderer::getScreenWidth(); + renderer.clearScreen(); + // Draw title renderer.drawCenteredText(UI_FONT_ID, startY, title.c_str(), true, REGULAR); // Draw input field - int inputY = startY + 22; + const int inputY = startY + 22; renderer.drawText(UI_FONT_ID, 10, inputY, "["); std::string displayText; @@ -204,9 +257,9 @@ void KeyboardEntryActivity::render(int startY) const { displayText += "_"; // Truncate if too long for display - use actual character width from font - int charWidth = renderer.getSpaceWidth(UI_FONT_ID); - if (charWidth < 1) charWidth = 8; // Fallback to approximate width - int maxDisplayLen = (pageWidth - 40) / charWidth; + int approxCharWidth = renderer.getSpaceWidth(UI_FONT_ID); + if (approxCharWidth < 1) approxCharWidth = 8; // Fallback to approximate width + const int maxDisplayLen = (pageWidth - 40) / approxCharWidth; if (displayText.length() > static_cast(maxDisplayLen)) { displayText = "..." + displayText.substr(displayText.length() - maxDisplayLen + 3); } @@ -215,22 +268,22 @@ void KeyboardEntryActivity::render(int startY) const { renderer.drawText(UI_FONT_ID, pageWidth - 15, inputY, "]"); // Draw keyboard - use compact spacing to fit 5 rows on screen - int keyboardStartY = inputY + 25; - const int keyWidth = 18; - const int keyHeight = 18; - const int keySpacing = 3; + const int keyboardStartY = inputY + 25; + constexpr int keyWidth = 18; + constexpr int keyHeight = 18; + constexpr int keySpacing = 3; const char* const* layout = shiftActive ? keyboardShift : keyboard; // Calculate left margin to center the longest row (13 keys) - int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing); - int leftMargin = (pageWidth - maxRowWidth) / 2; + constexpr int maxRowWidth = KEYS_PER_ROW * (keyWidth + keySpacing); + const int leftMargin = (pageWidth - maxRowWidth) / 2; for (int row = 0; row < NUM_ROWS; row++) { - int rowY = keyboardStartY + row * (keyHeight + keySpacing); + const int rowY = keyboardStartY + row * (keyHeight + keySpacing); // Left-align all rows for consistent navigation - int startX = leftMargin; + const int startX = leftMargin; // Handle bottom row (row 4) specially with proper multi-column keys if (row == 4) { @@ -240,64 +293,37 @@ void KeyboardEntryActivity::render(int startY) const { int currentX = startX; // CAPS key (logical col 0, spans 2 key widths) - int capsWidth = 2 * keyWidth + keySpacing; - bool capsSelected = (selectedRow == 4 && selectedCol == SHIFT_COL); - if (capsSelected) { - renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "["); - renderer.drawText(UI_FONT_ID, currentX + capsWidth - 4, rowY, "]"); - } - renderer.drawText(UI_FONT_ID, currentX + 2, rowY, shiftActive ? "CAPS" : "caps"); - currentX += capsWidth + keySpacing; + const bool capsSelected = (selectedRow == 4 && selectedCol >= SHIFT_COL && selectedCol < SPACE_COL); + renderItemWithSelector(currentX + 2, rowY, shiftActive ? "CAPS" : "caps", capsSelected); + currentX += 2 * (keyWidth + keySpacing); // Space bar (logical cols 2-6, spans 5 key widths) - int spaceWidth = 5 * keyWidth + 4 * keySpacing; - bool spaceSelected = (selectedRow == 4 && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL); - if (spaceSelected) { - renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "["); - renderer.drawText(UI_FONT_ID, currentX + spaceWidth - 4, rowY, "]"); - } - // Draw centered underscores for space bar - int spaceTextX = currentX + (spaceWidth / 2) - 12; - renderer.drawText(UI_FONT_ID, spaceTextX, rowY, "_____"); - currentX += spaceWidth + keySpacing; + const bool spaceSelected = (selectedRow == 4 && selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL); + const int spaceTextWidth = renderer.getTextWidth(UI_FONT_ID, "_____"); + const int spaceXWidth = 5 * (keyWidth + keySpacing); + const int spaceXPos = currentX + (spaceXWidth - spaceTextWidth) / 2; + renderItemWithSelector(spaceXPos, rowY, "_____", spaceSelected); + currentX += spaceXWidth; // Backspace key (logical col 7, spans 2 key widths) - int bsWidth = 2 * keyWidth + keySpacing; - bool bsSelected = (selectedRow == 4 && selectedCol == BACKSPACE_COL); - if (bsSelected) { - renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "["); - renderer.drawText(UI_FONT_ID, currentX + bsWidth - 4, rowY, "]"); - } - renderer.drawText(UI_FONT_ID, currentX + 6, rowY, "<-"); - currentX += bsWidth + keySpacing; + const bool bsSelected = (selectedRow == 4 && selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL); + renderItemWithSelector(currentX + 2, rowY, "<-", bsSelected); + currentX += 2 * (keyWidth + keySpacing); // OK button (logical col 9, spans 2 key widths) - int okWidth = 2 * keyWidth + keySpacing; - bool okSelected = (selectedRow == 4 && selectedCol >= DONE_COL); - if (okSelected) { - renderer.drawText(UI_FONT_ID, currentX - 2, rowY, "["); - renderer.drawText(UI_FONT_ID, currentX + okWidth - 4, rowY, "]"); - } - renderer.drawText(UI_FONT_ID, currentX + 8, rowY, "OK"); - + const bool okSelected = (selectedRow == 4 && selectedCol >= DONE_COL); + renderItemWithSelector(currentX + 2, rowY, "OK", okSelected); } else { // Regular rows: render each key individually for (int col = 0; col < getRowLength(row); col++) { - int keyX = startX + col * (keyWidth + keySpacing); - // Get the character to display - char c = layout[row][col]; + const char c = layout[row][col]; std::string keyLabel(1, c); + const int charWidth = renderer.getTextWidth(UI_FONT_ID, keyLabel.c_str()); - // Draw selection highlight - bool isSelected = (row == selectedRow && col == selectedCol); - - if (isSelected) { - renderer.drawText(UI_FONT_ID, keyX - 2, rowY, "["); - renderer.drawText(UI_FONT_ID, keyX + keyWidth - 4, rowY, "]"); - } - - renderer.drawText(UI_FONT_ID, keyX + 2, rowY, keyLabel.c_str()); + const int keyX = startX + col * (keyWidth + keySpacing) + (keyWidth - charWidth) / 2; + const bool isSelected = row == selectedRow && col == selectedCol; + renderItemWithSelector(keyX, rowY, keyLabel.c_str(), isSelected); } } } @@ -305,4 +331,15 @@ void KeyboardEntryActivity::render(int startY) const { // Draw help text at absolute bottom of screen (consistent with other screens) const auto pageHeight = GfxRenderer::getScreenHeight(); renderer.drawText(SMALL_FONT_ID, 10, pageHeight - 30, "Navigate: D-pad | Select: OK | Cancel: BACK"); + renderer.displayBuffer(); +} + +void KeyboardEntryActivity::renderItemWithSelector(const int x, const int y, const char* item, + const bool isSelected) const { + if (isSelected) { + const int itemWidth = renderer.getTextWidth(UI_FONT_ID, item); + renderer.drawText(UI_FONT_ID, x - 6, y, "["); + renderer.drawText(UI_FONT_ID, x + itemWidth, y, "]"); + } + renderer.drawText(UI_FONT_ID, x, y, item); } diff --git a/src/activities/util/KeyboardEntryActivity.h b/src/activities/util/KeyboardEntryActivity.h index 3b5b806..552a3e8 100644 --- a/src/activities/util/KeyboardEntryActivity.h +++ b/src/activities/util/KeyboardEntryActivity.h @@ -1,9 +1,13 @@ #pragma once #include #include +#include +#include +#include #include #include +#include #include "../Activity.h" @@ -30,80 +34,44 @@ class KeyboardEntryActivity : public Activity { * @param inputManager Reference to InputManager for handling input * @param title Title to display above the keyboard * @param initialText Initial text to show in the input field + * @param startY Y position to start rendering the keyboard * @param maxLength Maximum length of input text (0 for unlimited) * @param isPassword If true, display asterisks instead of actual characters + * @param onComplete Callback invoked when input is complete + * @param onCancel Callback invoked when input is cancelled */ - KeyboardEntryActivity(GfxRenderer& renderer, InputManager& inputManager, const std::string& title = "Enter Text", - const std::string& initialText = "", const size_t maxLength = 0, const bool isPassword = false) + explicit KeyboardEntryActivity(GfxRenderer& renderer, InputManager& inputManager, std::string title = "Enter Text", + std::string initialText = "", const int startY = 10, const size_t maxLength = 0, + const bool isPassword = false, OnCompleteCallback onComplete = nullptr, + OnCancelCallback onCancel = nullptr) : Activity("KeyboardEntry", renderer, inputManager), - title(title), - text(initialText), + title(std::move(title)), + text(std::move(initialText)), + startY(startY), maxLength(maxLength), - isPassword(isPassword) {} - - /** - * Handle button input. Call this in your main loop. - * @return true if input was handled, false otherwise - */ - bool handleInput(); - - /** - * Render the keyboard at the specified Y position. - * @param startY Y-coordinate where keyboard rendering starts (default 10) - */ - void render(int startY = 10) const; - - /** - * Get the current text entered by the user. - */ - const std::string& getText() const { return text; } - - /** - * Set the current text. - */ - void setText(const std::string& newText); - - /** - * Check if the user has completed text entry (pressed OK on Done). - */ - bool isComplete() const { return complete; } - - /** - * Check if the user has cancelled text entry. - */ - bool isCancelled() const { return cancelled; } - - /** - * Reset the keyboard state for reuse. - */ - void reset(const std::string& newTitle = "", const std::string& newInitialText = ""); - - /** - * Set callback for when input is complete. - */ - void setOnComplete(OnCompleteCallback callback) { onComplete = callback; } - - /** - * Set callback for when input is cancelled. - */ - void setOnCancel(OnCancelCallback callback) { onCancel = callback; } + isPassword(isPassword), + onComplete(std::move(onComplete)), + onCancel(std::move(onCancel)) {} // Activity overrides void onEnter() override; + void onExit() override; void loop() override; private: std::string title; + int startY; std::string text; size_t maxLength; bool isPassword; + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + bool updateRequired = false; // Keyboard state int selectedRow = 0; int selectedCol = 0; bool shiftActive = false; - bool complete = false; - bool cancelled = false; // Callbacks OnCompleteCallback onComplete; @@ -116,16 +84,17 @@ class KeyboardEntryActivity : public Activity { static const char* const keyboardShift[NUM_ROWS]; // Special key positions (bottom row) - static constexpr int SHIFT_ROW = 4; + static constexpr int SPECIAL_ROW = 4; static constexpr int SHIFT_COL = 0; - static constexpr int SPACE_ROW = 4; static constexpr int SPACE_COL = 2; - static constexpr int BACKSPACE_ROW = 4; static constexpr int BACKSPACE_COL = 7; - static constexpr int DONE_ROW = 4; static constexpr int DONE_COL = 9; + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); char getSelectedChar() const; void handleKeyPress(); int getRowLength(int row) const; + void render() const; + void renderItemWithSelector(int x, int y, const char* item, bool isSelected) const; };