#include "EpubWordSelectionActivity.h" #include #include #include #include #include "DictionaryMargins.h" #include "MappedInputManager.h" #include "fontIds.h" void EpubWordSelectionActivity::taskTrampoline(void* param) { auto* self = static_cast(param); self->displayTaskLoop(); } void EpubWordSelectionActivity::onEnter() { Activity::onEnter(); renderingMutex = xSemaphoreCreateMutex(); selectedWordIndex = 0; currentLineIndex = 0; // Build list of all words on the page buildWordList(); updateRequired = true; xTaskCreate(&EpubWordSelectionActivity::taskTrampoline, "WordSelectTask", 4096, // Stack size this, // Parameters 1, // Priority &displayTaskHandle // Task handle ); } void EpubWordSelectionActivity::onExit() { Activity::onExit(); xSemaphoreTake(renderingMutex, portMAX_DELAY); if (displayTaskHandle) { vTaskDelete(displayTaskHandle); displayTaskHandle = nullptr; vTaskDelay(10 / portTICK_PERIOD_MS); // Let idle task free stack } vSemaphoreDelete(renderingMutex); renderingMutex = nullptr; } void EpubWordSelectionActivity::buildWordList() { allWords.clear(); if (!page) return; const int lineHeight = renderer.getLineHeight(fontId); for (const auto& element : page->elements) { // All page elements are PageLine (only type in PageElementTag enum) const auto* pageLine = static_cast(element.get()); if (!pageLine) continue; const auto& textBlock = pageLine->getTextBlock(); if (!textBlock || textBlock->getWordCount() == 0) { continue; } const auto& words = textBlock->getWords(); const auto& xPositions = textBlock->getWordXPositions(); const auto& styles = textBlock->getWordStyles(); auto wordIt = words.begin(); auto xPosIt = xPositions.begin(); auto styleIt = styles.begin(); while (wordIt != words.end() && xPosIt != xPositions.end() && styleIt != styles.end()) { // Skip whitespace-only words const std::string& wordText = *wordIt; const bool hasAlpha = std::any_of(wordText.begin(), wordText.end(), [](char c) { return std::isalpha(static_cast(c)); }); if (hasAlpha) { WordInfo info; info.text = wordText; info.x = *xPosIt + pageLine->xPos + xOffset; info.y = pageLine->yPos + yOffset; info.width = renderer.getTextWidth(fontId, wordText.c_str(), *styleIt); info.height = lineHeight; info.style = *styleIt; allWords.push_back(info); } ++wordIt; ++xPosIt; ++styleIt; } } } int EpubWordSelectionActivity::findLineForWordIndex(int wordIndex) const { if (wordIndex < 0 || wordIndex >= static_cast(allWords.size())) return 0; int lineIdx = 0; int lastY = -1; for (size_t i = 0; i <= static_cast(wordIndex); i++) { if (allWords[i].y != lastY) { if (lastY >= 0) lineIdx++; lastY = allWords[i].y; } } return lineIdx; } int EpubWordSelectionActivity::findWordIndexForLine(int lineIndex) const { if (allWords.empty()) return 0; int currentLine = 0; int lastY = allWords[0].y; for (size_t i = 0; i < allWords.size(); i++) { if (allWords[i].y != lastY) { currentLine++; lastY = allWords[i].y; } if (currentLine == lineIndex) { return static_cast(i); } } // If line not found, return last word return static_cast(allWords.size()) - 1; } void EpubWordSelectionActivity::loop() { if (allWords.empty()) { onCancel(); return; } // Handle back button - cancel // Use wasReleased to consume the full button event if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onCancel(); return; } // Handle confirm button - select current word // Use wasReleased to consume the full button event if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { // Clean up the word (remove leading/trailing punctuation) std::string selectedWord = allWords[selectedWordIndex].text; // Strip em-space prefix if present if (selectedWord.size() >= 3 && static_cast(selectedWord[0]) == 0xE2 && static_cast(selectedWord[1]) == 0x80 && static_cast(selectedWord[2]) == 0x83) { selectedWord = selectedWord.substr(3); } // Strip leading/trailing non-alpha characters while (!selectedWord.empty() && !std::isalpha(static_cast(selectedWord.front()))) { selectedWord.erase(0, 1); } while (!selectedWord.empty() && !std::isalpha(static_cast(selectedWord.back()))) { selectedWord.pop_back(); } if (!selectedWord.empty()) { onWordSelected(selectedWord); } else { onCancel(); } return; } // Handle navigation const bool leftPressed = mappedInput.wasPressed(MappedInputManager::Button::Left); const bool rightPressed = mappedInput.wasPressed(MappedInputManager::Button::Right); const bool upPressed = mappedInput.wasPressed(MappedInputManager::Button::Up); const bool downPressed = mappedInput.wasPressed(MappedInputManager::Button::Down); if (leftPressed && selectedWordIndex > 0) { selectedWordIndex--; currentLineIndex = findLineForWordIndex(selectedWordIndex); updateRequired = true; } else if (rightPressed && selectedWordIndex < static_cast(allWords.size()) - 1) { selectedWordIndex++; currentLineIndex = findLineForWordIndex(selectedWordIndex); updateRequired = true; } else if (upPressed) { // Move to previous line if (currentLineIndex > 0) { currentLineIndex--; selectedWordIndex = findWordIndexForLine(currentLineIndex); updateRequired = true; } } else if (downPressed) { // Move to next line const int lastLine = findLineForWordIndex(static_cast(allWords.size()) - 1); if (currentLineIndex < lastLine) { currentLineIndex++; selectedWordIndex = findWordIndexForLine(currentLineIndex); updateRequired = true; } } } void EpubWordSelectionActivity::displayTaskLoop() { while (true) { if (updateRequired) { updateRequired = false; xSemaphoreTake(renderingMutex, portMAX_DELAY); render(); xSemaphoreGive(renderingMutex); } vTaskDelay(10 / portTICK_PERIOD_MS); } } void EpubWordSelectionActivity::render() const { renderer.clearScreen(); // Get margins using same pattern as reader + button hint space int marginTop, marginRight, marginBottom, marginLeft; getDictionaryContentMargins(renderer, &marginTop, &marginRight, &marginBottom, &marginLeft); // Draw the page content (uses pre-calculated offsets from reader) // The page already has proper offsets, so render as-is if (page) { page->render(renderer, fontId, xOffset, yOffset); } // Highlight the selected word with an inverted rectangle if (!allWords.empty() && selectedWordIndex >= 0 && selectedWordIndex < static_cast(allWords.size())) { const WordInfo& selected = allWords[selectedWordIndex]; // Draw selection box (inverted colors) constexpr int padding = 2; renderer.fillRect(selected.x - padding, selected.y - padding, selected.width + padding * 2, selected.height + padding * 2); // Redraw the word in white on black renderer.drawText(fontId, selected.x, selected.y, selected.text.c_str(), false, selected.style); } // Draw instruction text - position it just above the front button area const auto screenHeight = renderer.getScreenHeight(); renderer.drawCenteredText(SMALL_FONT_ID, screenHeight - marginBottom - 10, "Navigate with arrows, select with confirm"); // Draw button hints const auto labels = mappedInput.mapLabels("\xc2\xab Cancel", "Select", "< >", ""); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(EInkDisplay::FAST_REFRESH); }