diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index a3dc5b6e6..5641ae488 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -236,24 +236,10 @@ void Screen::showTextInput(const char *header, const char *initialText, uint32_t { LOG_INFO("showTextInput called with header='%s', durationMs=%d", header ? header : "NULL", durationMs); - if (NotificationRenderer::virtualKeyboard) { - delete NotificationRenderer::virtualKeyboard; - NotificationRenderer::virtualKeyboard = nullptr; - } - - NotificationRenderer::textInputCallback = nullptr; - - NotificationRenderer::virtualKeyboard = new VirtualKeyboard(); - if (header) { - NotificationRenderer::virtualKeyboard->setHeader(header); - } - if (initialText) { - NotificationRenderer::virtualKeyboard->setInputText(initialText); - } - - // Set up callback with safer cleanup mechanism + // Start OnScreenKeyboardModule session (non-touch variant) + OnScreenKeyboardModule::instance().start(header, initialText, durationMs, textCallback); + NotificationRenderer::virtualKeyboard = OnScreenKeyboardModule::instance().getKeyboard(); NotificationRenderer::textInputCallback = textCallback; - NotificationRenderer::virtualKeyboard->setCallback([textCallback](const std::string &text) { textCallback(text); }); // Store the message and set the expiration timestamp (use same pattern as other notifications) strncpy(NotificationRenderer::alertBannerMessage, header ? header : "Text Input", 255); diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 3ed0f33dc..e0f024612 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -50,11 +50,6 @@ std::function NotificationRenderer::alertBannerCallback = NULL; bool NotificationRenderer::pauseBanner = false; notificationTypeEnum NotificationRenderer::current_notification_type = notificationTypeEnum::none; -// Variables for message popup overlay on virtual keyboard -char NotificationRenderer::keyboardPopupMessage[256] = {0}; -char NotificationRenderer::keyboardPopupTitle[64] = {0}; -uint32_t NotificationRenderer::keyboardPopupUntil = 0; -bool NotificationRenderer::showKeyboardPopup = false; uint32_t NotificationRenderer::numDigits = 0; uint32_t NotificationRenderer::currentNumber = 0; VirtualKeyboard *NotificationRenderer::virtualKeyboard = nullptr; @@ -96,11 +91,7 @@ void NotificationRenderer::resetBanner() alertBannerMessage[0] = '\0'; current_notification_type = notificationTypeEnum::none; - // Reset keyboard popup message variables - keyboardPopupMessage[0] = '\0'; - keyboardPopupTitle[0] = '\0'; - keyboardPopupUntil = 0; - showKeyboardPopup = false; + OnScreenKeyboardModule::instance().clearPopup(); inEvent.inputEvent = INPUT_BROKER_NONE; inEvent.kbchar = 0; @@ -115,9 +106,10 @@ void NotificationRenderer::resetBanner() nodeDB->pause_sort(false); - // If we're exiting from text_input (virtual keyboard), trigger frame update + // If we're exiting from text_input (virtual keyboard), stop module and trigger frame update // to ensure any messages received during keyboard use are now displayed if (previousType == notificationTypeEnum::text_input && screen) { + OnScreenKeyboardModule::instance().stop(false); screen->setFrames(graphics::Screen::FOCUS_PRESERVE); } } @@ -629,97 +621,26 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi void NotificationRenderer::drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state) { - if (virtualKeyboard) { - // Check for timeout and auto-exit if needed - if (virtualKeyboard->isTimedOut()) { - LOG_INFO("Virtual keyboard timeout - auto-exiting"); - // Cancel virtual keyboard - call callback with empty string to indicate timeout - auto callback = textInputCallback; // Store callback before clearing + // Delegate session to OnScreenKeyboardModule + auto &osk = OnScreenKeyboardModule::instance(); - // Clean up first to prevent re-entry - delete virtualKeyboard; - virtualKeyboard = nullptr; - textInputCallback = nullptr; - resetBanner(); - - // Call callback after cleanup - if (callback) { - callback(""); - } - - // Restore normal overlays - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - return; - } - - if (inEvent.inputEvent != INPUT_BROKER_NONE) { - if (inEvent.inputEvent == INPUT_BROKER_UP) { - // high frequency for move cursor left/right than up/down with encoders - extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1; - extern ::UpDownInterruptImpl1 *upDownInterruptImpl1; - if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) { - virtualKeyboard->moveCursorLeft(); - } else { - virtualKeyboard->moveCursorUp(); - } - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN) { - extern ::RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1; - extern ::UpDownInterruptImpl1 *upDownInterruptImpl1; - if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) { - virtualKeyboard->moveCursorRight(); - } else { - virtualKeyboard->moveCursorDown(); - } - } else if (inEvent.inputEvent == INPUT_BROKER_LEFT) { - virtualKeyboard->moveCursorLeft(); - } else if (inEvent.inputEvent == INPUT_BROKER_RIGHT) { - virtualKeyboard->moveCursorRight(); - } else if (inEvent.inputEvent == INPUT_BROKER_UP_LONG) { - virtualKeyboard->moveCursorUp(); - } else if (inEvent.inputEvent == INPUT_BROKER_DOWN_LONG) { - virtualKeyboard->moveCursorDown(); - } else if (inEvent.inputEvent == INPUT_BROKER_ALT_PRESS) { - virtualKeyboard->moveCursorLeft(); - } else if (inEvent.inputEvent == INPUT_BROKER_USER_PRESS) { - virtualKeyboard->moveCursorRight(); - } else if (inEvent.inputEvent == INPUT_BROKER_SELECT) { - virtualKeyboard->handlePress(); - } else if (inEvent.inputEvent == INPUT_BROKER_SELECT_LONG) { - virtualKeyboard->handleLongPress(); - } else if (inEvent.inputEvent == INPUT_BROKER_CANCEL) { - auto callback = textInputCallback; - delete virtualKeyboard; - virtualKeyboard = nullptr; - textInputCallback = nullptr; - resetBanner(); - if (callback) { - callback(""); - } - if (screen) { - screen->setFrames(graphics::Screen::FOCUS_PRESERVE); - } - return; - } - - // Consume the event after processing for virtual keyboard - inEvent.inputEvent = INPUT_BROKER_NONE; - } - - // Clear the screen to avoid overlapping with underlying frames or overlays - display->setColor(BLACK); - display->fillRect(0, 0, display->getWidth(), display->getHeight()); - display->setColor(WHITE); - // Draw the virtual keyboard - virtualKeyboard->draw(display, 0, 0); - - // Draw message popup overlay if active - drawKeyboardMessagePopup(display); - } else { - // If virtualKeyboard is null, reset the banner to avoid getting stuck - LOG_INFO("Virtual keyboard is null - resetting banner"); + if (!osk.isActive()) { + LOG_INFO("Virtual keyboard is not active - resetting banner"); resetBanner(); + return; + } + + if (inEvent.inputEvent != INPUT_BROKER_NONE) { + osk.handleInput(inEvent); + inEvent.inputEvent = INPUT_BROKER_NONE; // consume + } + + // Draw. If draw returns false, session ended (timeout or cancel) + if (!osk.draw(display)) { + // Session ended, ensure banner reset and restore frames + resetBanner(); + if (screen) + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); } } @@ -730,135 +651,12 @@ bool NotificationRenderer::isOverlayBannerShowing() void NotificationRenderer::showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs) { - if (!title || !content || current_notification_type != notificationTypeEnum::text_input) { + if (!title || !content || current_notification_type != notificationTypeEnum::text_input) return; - } - - strncpy(keyboardPopupTitle, title, 63); - keyboardPopupTitle[63] = '\0'; - strncpy(keyboardPopupMessage, content, 255); - keyboardPopupMessage[255] = '\0'; - keyboardPopupUntil = millis() + durationMs; - showKeyboardPopup = true; + OnScreenKeyboardModule::instance().showPopup(title, content, durationMs); } -void NotificationRenderer::drawKeyboardMessagePopup(OLEDDisplay *display) -{ - // Check if popup should be dismissed - if (!showKeyboardPopup || millis() > keyboardPopupUntil || keyboardPopupMessage[0] == '\0') { - showKeyboardPopup = false; - return; - } - - // === Use drawNotificationBox for true BannerOverlayOptions style === - constexpr uint16_t maxContentLines = 3; // Maximum content lines - bool hasTitle = keyboardPopupTitle[0] != '\0'; - - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // Conservative wrap width to ensure we never exceed screen after padding - const uint16_t maxWrapWidth = display->width() - 40; // Leave room for padding and bell icons - - // Prepare content for display - build lines array for drawNotificationBox - std::vector allLines; - - // Parse message content into lines with word wrapping - char contentBuffer[256]; - strncpy(contentBuffer, keyboardPopupMessage, 255); - contentBuffer[255] = '\0'; - - // Helper function to wrap text to fit within available width - auto wrapText = [&](const char *text, uint16_t availableWidth) -> std::vector { - std::vector wrappedLines; - std::string currentLine; - std::string word; - const char *ptr = text; - - while (*ptr && wrappedLines.size() < maxContentLines) { - // Skip whitespace and handle explicit newlines - while (*ptr && (*ptr == ' ' || *ptr == '\t' || *ptr == '\n' || *ptr == '\r')) { - if (*ptr == '\n') { - if (!currentLine.empty()) { - wrappedLines.push_back(currentLine); - currentLine.clear(); - if (wrappedLines.size() >= maxContentLines) - break; - } - } - ++ptr; - } - if (!*ptr || wrappedLines.size() >= maxContentLines) - break; - - // Collect next word - word.clear(); - while (*ptr && *ptr != ' ' && *ptr != '\t' && *ptr != '\n' && *ptr != '\r') { - word += *ptr++; - } - if (word.empty()) - continue; - - // Try to append word - std::string testLine = currentLine.empty() ? word : (currentLine + " " + word); - uint16_t testWidth = display->getStringWidth(testLine.c_str(), testLine.length(), true); - if (testWidth <= availableWidth) { - currentLine = testLine; - } else { - if (!currentLine.empty()) { - wrappedLines.push_back(currentLine); - currentLine = word; - if (wrappedLines.size() >= maxContentLines) - break; - } else { - // Single word longer than available width: hard cut down to fit - currentLine = word; - while (currentLine.size() > 1 && - display->getStringWidth(currentLine.c_str(), currentLine.length(), true) > availableWidth) { - currentLine.pop_back(); - } - } - } - } - if (!currentLine.empty() && wrappedLines.size() < maxContentLines) { - wrappedLines.push_back(currentLine); - } - return wrappedLines; - }; - - // Add title if present - if (hasTitle) { - allLines.emplace_back(keyboardPopupTitle); - } - - // Split content by newlines first, then wrap each paragraph - std::vector contentLines; - char *paragraph = strtok(contentBuffer, "\n"); - while (paragraph != nullptr && contentLines.size() < maxContentLines) { - auto wrapped = wrapText(paragraph, maxWrapWidth); - for (const auto &ln : wrapped) { - if (contentLines.size() >= maxContentLines) - break; - contentLines.push_back(ln); - } - paragraph = strtok(nullptr, "\n"); - } - - // Add content lines to allLines - for (const auto &ln : contentLines) { - allLines.push_back(ln); - } - - // Convert to const char* array for drawNotificationBox - std::vector linePointers; - for (const auto &line : allLines) { - linePointers.push_back(line.c_str()); - } - linePointers.push_back(nullptr); // null terminate - - // Use a custom inverted color version for keyboard popup - drawInvertedNotificationBox(display, nullptr, linePointers.data(), allLines.size(), 0, 0); -} +// drawKeyboardMessagePopup removed; OnScreenKeyboardModule handles popup drawing within draw() // Custom inverted color version for keyboard popup - black background with white text void NotificationRenderer::drawInvertedNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[], diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index 20bbe6b0a..f45b49e2c 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -4,6 +4,7 @@ #include "OLEDDisplayUi.h" #include "graphics/Screen.h" #include "graphics/VirtualKeyboard.h" +#include "modules/OnScreenKeyboardModule.h" #include #include #define MAX_LINES 5 @@ -30,14 +31,8 @@ class NotificationRenderer static bool pauseBanner; - static char keyboardPopupMessage[256]; - static char keyboardPopupTitle[64]; - static uint32_t keyboardPopupUntil; - static bool showKeyboardPopup; - static void resetBanner(); static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs); - static void drawKeyboardMessagePopup(OLEDDisplay *display); static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state); diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 3a2189fad..295d063d8 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -1008,8 +1008,7 @@ int32_t CannedMessageModule::runOnce() if (graphics::NotificationRenderer::virtualKeyboard && graphics::NotificationRenderer::textInputCallback == nullptr && graphics::NotificationRenderer::current_notification_type != graphics::notificationTypeEnum::text_input) { LOG_INFO("Performing delayed virtual keyboard cleanup"); - delete graphics::NotificationRenderer::virtualKeyboard; - graphics::NotificationRenderer::virtualKeyboard = nullptr; + graphics::OnScreenKeyboardModule::instance().stop(false); } temporaryMessage = ""; @@ -1026,9 +1025,7 @@ int32_t CannedMessageModule::runOnce() // Clean up virtual keyboard after sending if (graphics::NotificationRenderer::virtualKeyboard) { LOG_INFO("Cleaning up virtual keyboard after message send"); - delete graphics::NotificationRenderer::virtualKeyboard; - graphics::NotificationRenderer::virtualKeyboard = nullptr; - graphics::NotificationRenderer::textInputCallback = nullptr; + graphics::OnScreenKeyboardModule::instance().stop(false); graphics::NotificationRenderer::resetBanner(); } @@ -1089,9 +1086,7 @@ int32_t CannedMessageModule::runOnce() LOG_INFO("Virtual keyboard is active - not cleaning up due to CannedMessage timeout"); } else if (graphics::NotificationRenderer::virtualKeyboard) { LOG_INFO("Cleaning up virtual keyboard due to module timeout"); - delete graphics::NotificationRenderer::virtualKeyboard; - graphics::NotificationRenderer::virtualKeyboard = nullptr; - graphics::NotificationRenderer::textInputCallback = nullptr; + graphics::OnScreenKeyboardModule::instance().stop(false); graphics::NotificationRenderer::resetBanner(); } diff --git a/src/modules/OnScreenKeyboardModule.cpp b/src/modules/OnScreenKeyboardModule.cpp new file mode 100644 index 000000000..04ed7a44d --- /dev/null +++ b/src/modules/OnScreenKeyboardModule.cpp @@ -0,0 +1,279 @@ +#include "configuration.h" +#if HAS_SCREEN + +#include "graphics/SharedUIDisplay.h" +#include "graphics/draw/NotificationRenderer.h" // drawInvertedNotificationBox signature reuse +#include "input/RotaryEncoderInterruptImpl1.h" +#include "input/UpDownInterruptImpl1.h" +#include "modules/OnScreenKeyboardModule.h" +#include +#include + +namespace graphics +{ + +OnScreenKeyboardModule &OnScreenKeyboardModule::instance() +{ + static OnScreenKeyboardModule inst; + return inst; +} + +OnScreenKeyboardModule::~OnScreenKeyboardModule() +{ + if (keyboard) { + delete keyboard; + keyboard = nullptr; + } +} + +void OnScreenKeyboardModule::start(const char *header, const char *initialText, uint32_t, + std::function cb) +{ + if (keyboard) { + delete keyboard; + keyboard = nullptr; + } + keyboard = new VirtualKeyboard(); + callback = cb; + if (header) + keyboard->setHeader(header); + if (initialText) + keyboard->setInputText(initialText); + + // Route VK submission/cancel events back into the module + keyboard->setCallback([this](const std::string &text) { + if (text.empty()) { + this->onCancel(); + } else { + this->onSubmit(text); + } + }); + + // Maintain legacy compatibility hooks + NotificationRenderer::virtualKeyboard = keyboard; + NotificationRenderer::textInputCallback = callback; +} + +void OnScreenKeyboardModule::stop(bool callEmptyCallback) +{ + auto cb = callback; + callback = nullptr; + if (keyboard) { + delete keyboard; + keyboard = nullptr; + } + // Keep NotificationRenderer legacy pointers in sync + NotificationRenderer::virtualKeyboard = nullptr; + NotificationRenderer::textInputCallback = nullptr; + clearPopup(); + if (callEmptyCallback && cb) + cb(""); +} + +bool OnScreenKeyboardModule::isActive() const +{ + return keyboard != nullptr; +} + +void OnScreenKeyboardModule::handleInput(const InputEvent &event) +{ + if (!keyboard) + return; + + // Auto-timeout check handled in draw() to centralize state transitions. + switch (event.inputEvent) { + case INPUT_BROKER_UP: { + if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) + keyboard->moveCursorLeft(); + else + keyboard->moveCursorUp(); + break; + } + case INPUT_BROKER_DOWN: { + if (::rotaryEncoderInterruptImpl1 || ::upDownInterruptImpl1) + keyboard->moveCursorRight(); + else + keyboard->moveCursorDown(); + break; + } + case INPUT_BROKER_LEFT: + keyboard->moveCursorLeft(); + break; + case INPUT_BROKER_RIGHT: + keyboard->moveCursorRight(); + break; + case INPUT_BROKER_UP_LONG: + keyboard->moveCursorUp(); + break; + case INPUT_BROKER_DOWN_LONG: + keyboard->moveCursorDown(); + break; + case INPUT_BROKER_ALT_PRESS: + keyboard->moveCursorLeft(); + break; + case INPUT_BROKER_USER_PRESS: + keyboard->moveCursorRight(); + break; + case INPUT_BROKER_SELECT: + keyboard->handlePress(); + break; + case INPUT_BROKER_SELECT_LONG: + keyboard->handleLongPress(); + break; + case INPUT_BROKER_CANCEL: + onCancel(); + break; + default: + break; + } +} + +bool OnScreenKeyboardModule::draw(OLEDDisplay *display) +{ + if (!keyboard) + return false; + + // Timeout + if (keyboard->isTimedOut()) { + onCancel(); + return false; + } + + // Clear full screen behind keyboard + display->setColor(BLACK); + display->fillRect(0, 0, display->getWidth(), display->getHeight()); + display->setColor(WHITE); + keyboard->draw(display, 0, 0); + + // Draw popup overlay if needed + drawPopup(display); + return true; +} + +void OnScreenKeyboardModule::onSubmit(const std::string &text) +{ + auto cb = callback; + stop(false); + if (cb) + cb(text); +} + +void OnScreenKeyboardModule::onCancel() +{ + stop(true); +} + +void OnScreenKeyboardModule::showPopup(const char *title, const char *content, uint32_t durationMs) +{ + if (!title || !content) + return; + strncpy(popupTitle, title, sizeof(popupTitle) - 1); + popupTitle[sizeof(popupTitle) - 1] = '\0'; + strncpy(popupMessage, content, sizeof(popupMessage) - 1); + popupMessage[sizeof(popupMessage) - 1] = '\0'; + popupUntil = millis() + durationMs; + popupVisible = true; +} + +void OnScreenKeyboardModule::clearPopup() +{ + popupTitle[0] = '\0'; + popupMessage[0] = '\0'; + popupUntil = 0; + popupVisible = false; +} + +void OnScreenKeyboardModule::drawPopup(OLEDDisplay *display) +{ + if (!popupVisible) + return; + if (millis() > popupUntil || popupMessage[0] == '\0') { + popupVisible = false; + return; + } + + // Build lines and leverage NotificationRenderer inverted box drawing for consistent style + constexpr uint16_t maxContentLines = 3; + const bool hasTitle = popupTitle[0] != '\0'; + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + const uint16_t maxWrapWidth = display->width() - 40; + + auto wrapText = [&](const char *text, uint16_t availableWidth) -> std::vector { + std::vector wrapped; + std::string current; + std::string word; + const char *p = text; + while (*p && wrapped.size() < maxContentLines) { + while (*p && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) { + if (*p == '\n') { + if (!current.empty()) { + wrapped.push_back(current); + current.clear(); + if (wrapped.size() >= maxContentLines) + break; + } + } + ++p; + } + if (!*p || wrapped.size() >= maxContentLines) + break; + word.clear(); + while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') + word += *p++; + if (word.empty()) + continue; + std::string test = current.empty() ? word : (current + " " + word); + uint16_t w = display->getStringWidth(test.c_str(), test.length(), true); + if (w <= availableWidth) + current = test; + else { + if (!current.empty()) { + wrapped.push_back(current); + current = word; + if (wrapped.size() >= maxContentLines) + break; + } else { + current = word; + while (current.size() > 1 && + display->getStringWidth(current.c_str(), current.length(), true) > availableWidth) + current.pop_back(); + } + } + } + if (!current.empty() && wrapped.size() < maxContentLines) + wrapped.push_back(current); + return wrapped; + }; + + std::vector allLines; + if (hasTitle) + allLines.emplace_back(popupTitle); + + char buf[sizeof(popupMessage)]; + strncpy(buf, popupMessage, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + char *paragraph = strtok(buf, "\n"); + while (paragraph && allLines.size() < maxContentLines + (hasTitle ? 1 : 0)) { + auto wrapped = wrapText(paragraph, maxWrapWidth); + for (const auto &ln : wrapped) { + if (allLines.size() >= maxContentLines + (hasTitle ? 1 : 0)) + break; + allLines.push_back(ln); + } + paragraph = strtok(nullptr, "\n"); + } + + std::vector ptrs; + for (const auto &ln : allLines) + ptrs.push_back(ln.c_str()); + ptrs.push_back(nullptr); + + // Use the inverted notification box already present in NotificationRenderer + NotificationRenderer::drawInvertedNotificationBox(display, nullptr, ptrs.data(), allLines.size(), 0, 0); +} + +} // namespace graphics + +#endif // HAS_SCREEN diff --git a/src/modules/OnScreenKeyboardModule.h b/src/modules/OnScreenKeyboardModule.h new file mode 100644 index 000000000..8bcec9cc2 --- /dev/null +++ b/src/modules/OnScreenKeyboardModule.h @@ -0,0 +1,69 @@ +#pragma once + +#include "configuration.h" +#if HAS_SCREEN + +#include "graphics/Screen.h" // InputEvent +#include "graphics/VirtualKeyboard.h" +#include +#include +#include + +namespace graphics +{ + +// Lightweight UI module to manage on-screen keyboard (non-touch). +class OnScreenKeyboardModule +{ + public: + static OnScreenKeyboardModule &instance(); + + // Begin a keyboard session + void start(const char *header, const char *initialText, + uint32_t /*durationMs unused here - NotificationRenderer controls banner timeout*/, + std::function callback); + + // Stop current session (optionally call callback with empty string) + void stop(bool callEmptyCallback); + + // Session status + bool isActive() const; + + // Event handling + drawing + void handleInput(const InputEvent &event); + // Draw keyboard and any overlay popup; return false if session ended (timeout or submit/cancel) + bool draw(OLEDDisplay *display); + + // Popup helpers (title/content shown above keyboard) + void showPopup(const char *title, const char *content, uint32_t durationMs); + void clearPopup(); + + // Compatibility: expose underlying keyboard pointer for existing callsites + VirtualKeyboard *getKeyboard() const { return keyboard; } + + private: + OnScreenKeyboardModule() = default; + ~OnScreenKeyboardModule(); + OnScreenKeyboardModule(const OnScreenKeyboardModule &) = delete; + OnScreenKeyboardModule &operator=(const OnScreenKeyboardModule &) = delete; + + // Internal helpers + void onSubmit(const std::string &text); + void onCancel(); + + // Popup rendering + void drawPopup(OLEDDisplay *display); + + VirtualKeyboard *keyboard = nullptr; + std::function callback; + + // Popup state + char popupTitle[64] = {0}; + char popupMessage[256] = {0}; + uint32_t popupUntil = 0; + bool popupVisible = false; +}; + +} // namespace graphics + +#endif // HAS_SCREEN