From f6bf33d61db1e8d979f17ff61a3205cfaa4ea198 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 19 Feb 2026 17:09:28 -0500 Subject: [PATCH] Optimization --- src/modules/CannedMessageModule.cpp | 39 +++--- src/modules/CannedMessageModule.h | 7 + src/modules/CannedMessageShared.cpp | 195 +++++++++++++++++++--------- src/modules/FreeTextModule.cpp | 186 +++++++++++++++----------- 4 files changed, 270 insertions(+), 157 deletions(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 13a7e7b17..f01a6d872 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -210,6 +210,17 @@ void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, } } +// Reset transient compose state used by canned/freetext flows. +void CannedMessageModule::clearComposeState() +{ + this->currentMessageIndex = -1; + this->freetext = ""; + this->cursor = 0; + this->freeTextLayoutCacheText = ""; + this->freeTextLayoutCacheWidth = -1; + this->freeTextLayoutCacheLines.clear(); +} + // Returns true if character input is currently allowed (used for search/freetext states) bool CannedMessageModule::isCharInputAllowed() const { @@ -381,9 +392,7 @@ int32_t CannedMessageModule::runOnce() UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; + this->clearComposeState(); this->notifyObservers(&e); return 2000; } @@ -395,25 +404,19 @@ int32_t CannedMessageModule::runOnce() (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; + this->clearComposeState(); this->notifyObservers(&e); } // Handle SENDING_ACTIVE state transition after virtual keyboard message else if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE && this->payload == 0) { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; + this->clearComposeState(); return INT32_MAX; } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { // Reset module on inactivity e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; + this->clearComposeState(); this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; // Clean up virtual keyboard if it exists during timeout @@ -434,9 +437,7 @@ int32_t CannedMessageModule::runOnce() sendText(this->dest, this->channel, this->freetext.c_str(), true); // Clean up state but *don’t* deactivate yet - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; + this->clearComposeState(); // Tell Screen to jump straight to the TextMessage frame e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; @@ -461,9 +462,7 @@ int32_t CannedMessageModule::runOnce() sendText(this->dest, this->channel, this->messages[this->currentMessageIndex], true); // Clean up state - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; + this->clearComposeState(); // Tell Screen to jump straight to the TextMessage frame e.action = UIFrameEvent::Action::SWITCH_TO_TEXTMESSAGE; @@ -479,9 +478,7 @@ int32_t CannedMessageModule::runOnce() } } // fallback clean-up if nothing above returned - this->currentMessageIndex = -1; - this->freetext = ""; - this->cursor = 0; + this->clearComposeState(); e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->notifyObservers(&e); diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index f07be51ce..ebc562b56 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -2,6 +2,8 @@ #if HAS_SCREEN #include "ProtobufModule.h" #include "input/InputBroker.h" +#include +#include // ============================ // Enums & Defines @@ -155,6 +157,10 @@ class CannedMessageModule : public SinglePortModule, public Observable>> freeTextLayoutCacheLines; // When true, the next emote picker selection replaces this typed prefix span. bool freeTextEmoteReplacePending = false; unsigned int freeTextEmoteReplaceStart = 0; @@ -202,6 +208,7 @@ class CannedMessageModule : public SinglePortModule, public Observable= 'A' && c <= 'Z') { + return static_cast(c + ('a' - 'A')); + } + return c; +} + +// Case-insensitive substring search without temporary String allocations. +bool containsCaseInsensitiveAscii(const char *text, const char *needle) +{ + if (!text || !needle || !needle[0]) { + return true; + } + + const size_t textLen = strlen(text); + const size_t needleLen = strlen(needle); + if (needleLen > textLen) { + return false; + } + + for (size_t i = 0; i + needleLen <= textLen; ++i) { + bool matched = true; + for (size_t j = 0; j < needleLen; ++j) { + if (toLowerAscii(text[i + j]) != toLowerAscii(needle[j])) { + matched = false; + break; + } + } + if (matched) { + return true; + } + } + return false; +} + +// Truncate text to fit width and append ellipsis when clipping is required. +void truncateWithEllipsisToWidth(OLEDDisplay *display, char *text, size_t textSize, int maxWidth) +{ + if (!display || !text || textSize == 0) { + return; + } + if (maxWidth <= 0) { + text[0] = '\0'; + return; + } + if (display->getStringWidth(text) <= maxWidth) { + return; + } + if (textSize < 4) { + text[0] = '\0'; + return; + } + + const size_t originalLen = strlen(text); + size_t low = 0; + size_t high = std::min(originalLen, textSize - 4); + char candidate[64]; + candidate[0] = '\0'; + + // Binary search shortest-fitting clipped token with ellipsis. + while (low <= high) { + const size_t mid = low + ((high - low) / 2); + if (mid >= sizeof(candidate)) { + break; + } + memcpy(candidate, text, mid); + candidate[mid] = '\0'; + strcat(candidate, "..."); + if (display->getStringWidth(candidate) <= maxWidth) { + low = mid + 1; + } else { + if (mid == 0) { + break; + } + high = mid - 1; + } + } + + const size_t keep = (low == 0) ? 0 : (low - 1); + const size_t clipped = std::min(keep, textSize - 4); + text[clipped] = '\0'; + strcat(text, "..."); +} + +// Find the emote descriptor for a token label. +const graphics::Emote *findEmoteByLabel(const String &label) +{ + for (int i = 0; i < graphics::numEmotes; ++i) { + if (label == graphics::emotes[i].label) { + return &graphics::emotes[i]; + } + } + return nullptr; +} +} // namespace + // Reset destination search state and keep the previous selection roughly centered. void CannedMessageModule::resetSearch() { @@ -69,8 +169,8 @@ void CannedMessageModule::updateDestinationSelectionList() this->activeChannelIndices.clear(); NodeNum myNodeNum = nodeDB->getNodeNum(); - String lowerSearchQuery = searchQuery; - lowerSearchQuery.toLowerCase(); + const bool hasSearchFilter = searchQuery.length() > 0; + const char *searchText = searchQuery.c_str(); // Preallocate space to reduce reallocation this->filteredNodes.reserve(numMeshNodes); @@ -80,33 +180,35 @@ void CannedMessageModule::updateDestinationSelectionList() if (!node || node->num == myNodeNum || !node->has_user || node->user.public_key.size != 32) continue; - const String &nodeName = node->user.long_name; - - if (searchQuery.length() == 0) { + const char *nodeName = node->user.long_name; + if (!hasSearchFilter) { this->filteredNodes.push_back({node, sinceLastSeen(node)}); } else { - // Avoid unnecessary lowercase conversion if already matched - String lowerNodeName = nodeName; - lowerNodeName.toLowerCase(); - - if (lowerNodeName.indexOf(lowerSearchQuery) != -1) { + // Compare in place to avoid building lower-cased temporary Strings per node. + if (containsCaseInsensitiveAscii(nodeName, searchText)) { this->filteredNodes.push_back({node, sinceLastSeen(node)}); } } } - meshtastic_MeshPacket *p = allocDataPacket(); - p->pki_encrypted = true; - p->channel = 0; - - // Populate active channels - std::vector seenChannels; - seenChannels.reserve(channels.getNumChannels()); + // Populate unique active channels without temporary String lists. for (uint8_t i = 0; i < channels.getNumChannels(); ++i) { - String name = channels.getName(i); - if (name.length() > 0 && std::find(seenChannels.begin(), seenChannels.end(), name) == seenChannels.end()) { + const char *name = channels.getName(i); + if (!name || !name[0]) { + continue; + } + + bool alreadyIncluded = false; + for (uint8_t existingIndex : this->activeChannelIndices) { + const char *existingName = channels.getName(existingIndex); + if (existingName && strcmp(existingName, name) == 0) { + alreadyIncluded = true; + break; + } + } + + if (!alreadyIncluded) { this->activeChannelIndices.push_back(i); - seenChannels.push_back(name); } } @@ -569,14 +671,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O ((node && node->is_favorite) ? 10 : 0); if (availWidth < 0) availWidth = 0; - - size_t origLen = strlen(entryText); - while (entryText[0] && display->getStringWidth(entryText) > availWidth) { - entryText[strlen(entryText) - 1] = '\0'; - } - if (strlen(entryText) < origLen) { - strcat(entryText, "..."); - } + truncateWithEllipsisToWidth(display, entryText, sizeof(entryText), availWidth); // Prepend "* " if this is a favorite if (node && node->is_favorite) { @@ -659,7 +754,6 @@ void CannedMessageModule::drawCannedMessageListScreen(OLEDDisplay *display, OLED const int baseRowSpacing = FONT_HEIGHT_SMALL - 4; int topMsg; - std::vector rowHeights; int _visibleRows; // Draw header (To: ...). @@ -669,40 +763,32 @@ void CannedMessageModule::drawCannedMessageListScreen(OLEDDisplay *display, OLED const int listYOffset = y + FONT_HEIGHT_SMALL - 3; _visibleRows = (display->getHeight() - listYOffset) / baseRowSpacing; - // Figure out which messages are visible and their needed heights. + // Figure out which messages are visible. topMsg = (messagesCount > _visibleRows && currentMessageIndex >= _visibleRows - 1) ? currentMessageIndex - _visibleRows + 2 : 0; int countRows = std::min(messagesCount, _visibleRows); - // Build per-row max height based on all emotes in line. - for (int i = 0; i < countRows; i++) { - const char *msg = getMessageByIndex(topMsg + i); - int maxEmoteHeight = 0; - for (int j = 0; j < graphics::numEmotes; j++) { - const char *label = graphics::emotes[j].label; - if (!label || !*label) - continue; - const char *search = msg; - while ((search = strstr(search, label))) { - if (graphics::emotes[j].height > maxEmoteHeight) - maxEmoteHeight = graphics::emotes[j].height; - search += strlen(label); // Advance past this emote. - } - } - rowHeights.push_back(std::max(baseRowSpacing, maxEmoteHeight + 2)); - } - // Draw all message rows with multi-emote support. int yCursor = listYOffset; for (int vis = 0; vis < countRows; vis++) { int msgIdx = topMsg + vis; int lineY = yCursor; const char *msg = getMessageByIndex(msgIdx); - int rowHeight = rowHeights[vis]; bool _highlight = (msgIdx == currentMessageIndex); // Multi-emote tokenization. std::vector> tokens = freeTextModule::tokenizeMessageWithEmotes(msg); + int maxEmoteHeight = 0; + for (const auto &token : tokens) { + if (!token.first) { + continue; + } + const graphics::Emote *emote = findEmoteByLabel(token.second); + if (emote && emote->height > maxEmoteHeight) { + maxEmoteHeight = emote->height; + } + } + const int rowHeight = std::max(baseRowSpacing, maxEmoteHeight + 2); // Vertically center based on rowHeight. int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2; @@ -871,14 +957,7 @@ ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket & display->getWidth() - ((graphics::currentResolution == graphics::ScreenResolution::High) ? 60 : 30); if (availWidth < 0) availWidth = 0; - - size_t origLen = strlen(nodeName); - while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) { - nodeName[strlen(nodeName) - 1] = '\0'; - } - if (strlen(nodeName) < origLen) { - strcat(nodeName, "..."); - } + truncateWithEllipsisToWidth(display, nodeName, sizeof(nodeName), availWidth); // Calculate signal quality and bars based on preset, SNR, and RSSI float snrLimit = getSnrLimit(config.lora.modem_preset); @@ -917,4 +996,4 @@ ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket & return ProcessMessage::CONTINUE; } -#endif +#endif \ No newline at end of file diff --git a/src/modules/FreeTextModule.cpp b/src/modules/FreeTextModule.cpp index 3c8677f07..3425dca18 100644 --- a/src/modules/FreeTextModule.cpp +++ b/src/modules/FreeTextModule.cpp @@ -20,6 +20,7 @@ static constexpr int32_t kInactivateAfterMs = 20000; std::vector> freeTextModule::tokenizeMessageWithEmotes(const char *msg) { std::vector> tokens; + tokens.reserve(8); int msgLen = strlen(msg); int pos = 0; while (pos < msgLen) { @@ -597,8 +598,8 @@ bool CannedMessageModule::acceptFreeTextCompletion(bool appendSpace) String prefixLower = getFreeTextPrefix(); const bool isEmoteCompletion = (this->freeTextCompletion == kFreeTextEmoteCompletion); - if (prefixLower.length() == 0 || this->cursor < prefixLower.length() || - (!isEmoteCompletion && prefixLower.length() < 2) || this->freeTextCompletion.length() <= prefixLower.length()) { + if (prefixLower.length() == 0 || this->cursor < prefixLower.length() || (!isEmoteCompletion && prefixLower.length() < 2) || + this->freeTextCompletion.length() <= prefixLower.length()) { return false; } @@ -614,7 +615,7 @@ bool CannedMessageModule::acceptFreeTextCompletion(bool appendSpace) this->runState = CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER; this->requestFocus(); if (screen) { - screen->forceDisplay(); + screen->forceDisplay(true); } return true; } @@ -713,7 +714,8 @@ void CannedMessageModule::drawFreeTextCompletionRow(OLEDDisplay *display, int16_ return -1; }; - std::vector choices; + ChoiceLayout choices[maxFreeTextCompletions]; + uint8_t choiceCount = 0; int runningX = 0; bool hasChoices = false; int selectedStart = 0; @@ -736,7 +738,9 @@ void CannedMessageModule::drawFreeTextCompletionRow(OLEDDisplay *display, int16_ const int emoteIndex = findEmoteIndex(candidate); int tokenWidth = (emoteIndex >= 0) ? graphics::emotes[emoteIndex].width : display->getStringWidth(candidate); int chipWidth = tokenWidth + (chipPaddingX * 2); - choices.push_back({i, candidate, runningX, chipWidth, emoteIndex}); + if (choiceCount < maxFreeTextCompletions) { + choices[choiceCount++] = {i, candidate, runningX, chipWidth, emoteIndex}; + } if (i == this->freeTextCompletionIndex) { selectedStart = runningX; selectedEnd = runningX + chipWidth; @@ -762,7 +766,8 @@ void CannedMessageModule::drawFreeTextCompletionRow(OLEDDisplay *display, int16_ const int centeredOffsetX = (runningX < viewportWidth) ? ((viewportWidth - runningX) / 2) : 0; const int drawBaseX = x + centeredOffsetX; - for (const auto &choice : choices) { + for (uint8_t i = 0; i < choiceCount; ++i) { + const ChoiceLayout &choice = choices[i]; int boxX = drawBaseX + (choice.startX - choiceScrollX); if ((boxX + choice.width) < x || boxX > (x + viewportWidth)) { continue; @@ -872,8 +877,8 @@ int32_t CannedMessageModule::runFreeTextState(UIFrameEvent &e) const size_t emoteLen = emoteLabelLengthEndingAtCursor(this->freetext, this->cursor); const unsigned int deleteLen = (emoteLen > 0) ? static_cast(emoteLen) : 1; const unsigned int deleteStart = this->cursor - deleteLen; - this->freetext = this->freetext.substring(0, deleteStart) + - this->freetext.substring(this->cursor, this->freetext.length()); + this->freetext = + this->freetext.substring(0, deleteStart) + this->freetext.substring(this->cursor, this->freetext.length()); this->cursor = deleteStart; } break; @@ -990,7 +995,8 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) this->freeTextEmoteReplaceLength = 0; runState = CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER; requestFocus(); - screen->forceDisplay(); + // Force immediate UI refresh so picker appears/snaps quickly on low idle FPS. + screen->forceDisplay(true); return true; } @@ -1098,12 +1104,14 @@ int CannedMessageModule::handleEmotePickerInput(const InputEvent *event) if (isUp && emotePickerIndex > 0) { emotePickerIndex--; - screen->forceDisplay(); + // Non-EInk builds need forceUiUpdate=true for responsive per-step redraw. + screen->forceDisplay(true); return 1; } if (isDown && emotePickerIndex < numEmotes - 1) { emotePickerIndex++; - screen->forceDisplay(); + // Non-EInk builds need forceUiUpdate=true for responsive per-step redraw. + screen->forceDisplay(true); return 1; } @@ -1128,7 +1136,7 @@ int CannedMessageModule::handleEmotePickerInput(const InputEvent *event) this->freeTextEmoteReplaceStart = 0; this->freeTextEmoteReplaceLength = 0; runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - screen->forceDisplay(); + screen->forceDisplay(true); return 1; } @@ -1140,7 +1148,7 @@ int CannedMessageModule::handleEmotePickerInput(const InputEvent *event) this->freeTextCompletionSuppressed = false; this->updateFreeTextCompletion(); runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - screen->forceDisplay(); + screen->forceDisplay(true); return 1; } @@ -1256,70 +1264,78 @@ void CannedMessageModule::drawFreeTextScreen(OLEDDisplay *display, OLEDDisplayUi int inputY = y + FONT_HEIGHT_SMALL + inputTopOffset; String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor); - // Tokenize input into (isEmote, token) pairs. - const char *msg = msgWithCursor.c_str(); - std::vector> tokens = freeTextModule::tokenizeMessageWithEmotes(msg); + // Rebuild wrapped token lines only when text or viewport width changes. + const int maxWidth = display->getWidth(); + if (this->freeTextLayoutCacheWidth != maxWidth || this->freeTextLayoutCacheText != msgWithCursor) { + this->freeTextLayoutCacheWidth = maxWidth; + this->freeTextLayoutCacheText = msgWithCursor; + this->freeTextLayoutCacheLines.clear(); - // Advanced word-wrapping (emotes + text, split by word, wrap inside word if needed). - std::vector>> lines; - std::vector> currentLine; - int lineWidth = 0; - int maxWidth = display->getWidth(); - for (auto &token : tokens) { - if (token.first) { - int tokenWidth = 0; - for (int j = 0; j < graphics::numEmotes; j++) { - if (token.second == graphics::emotes[j].label) { - tokenWidth = graphics::emotes[j].width + 2; - break; - } - } - if (lineWidth + tokenWidth > maxWidth && !currentLine.empty()) { - lines.push_back(currentLine); + const char *msg = msgWithCursor.c_str(); + std::vector> tokens = freeTextModule::tokenizeMessageWithEmotes(msg); + std::vector> currentLine; + currentLine.reserve(8); + int lineWidth = 0; + + // Commit the current line and reset wrapping state. + auto flushLine = [this, ¤tLine, &lineWidth]() { + if (!currentLine.empty()) { + this->freeTextLayoutCacheLines.push_back(currentLine); currentLine.clear(); lineWidth = 0; } - currentLine.push_back(token); - lineWidth += tokenWidth; - } else { - // Text: split by words and wrap inside word if needed. - String text = token.second; - int pos = 0; - while (pos < static_cast(text.length())) { - int spacePos = text.indexOf(' ', pos); - int endPos = (spacePos == -1) ? text.length() : spacePos + 1; // Include space. - String word = text.substring(pos, endPos); - int wordWidth = display->getStringWidth(word); + }; - if (lineWidth + wordWidth > maxWidth && lineWidth > 0) { - lines.push_back(currentLine); - currentLine.clear(); - lineWidth = 0; - } - if (wordWidth > maxWidth) { - uint16_t charPos = 0; - while (charPos < word.length()) { - String oneChar = word.substring(charPos, charPos + 1); - int charWidth = display->getStringWidth(oneChar); - if (lineWidth + charWidth > maxWidth && lineWidth > 0) { - lines.push_back(currentLine); - currentLine.clear(); - lineWidth = 0; - } - currentLine.push_back({false, oneChar}); - lineWidth += charWidth; - charPos++; + for (const auto &token : tokens) { + if (token.first) { + int tokenWidth = 0; + for (int j = 0; j < graphics::numEmotes; j++) { + if (token.second == graphics::emotes[j].label) { + tokenWidth = graphics::emotes[j].width + 2; + break; } - } else { - currentLine.push_back({false, word}); - lineWidth += wordWidth; } - pos = endPos; + if (lineWidth + tokenWidth > maxWidth && !currentLine.empty()) { + flushLine(); + } + currentLine.push_back(token); + lineWidth += tokenWidth; + } else { + // Text: split by words and wrap inside a long word when needed. + const String &text = token.second; + int pos = 0; + while (pos < static_cast(text.length())) { + int spacePos = text.indexOf(' ', pos); + int endPos = (spacePos == -1) ? text.length() : (spacePos + 1); // Include trailing space. + String word = text.substring(pos, endPos); + int wordWidth = display->getStringWidth(word); + + if (lineWidth + wordWidth > maxWidth && lineWidth > 0) { + flushLine(); + } + if (wordWidth > maxWidth) { + uint16_t charPos = 0; + while (charPos < word.length()) { + String oneChar = word.substring(charPos, charPos + 1); + int charWidth = display->getStringWidth(oneChar); + if (lineWidth + charWidth > maxWidth && lineWidth > 0) { + flushLine(); + } + currentLine.push_back({false, oneChar}); + lineWidth += charWidth; + charPos++; + } + } else { + currentLine.push_back({false, word}); + lineWidth += wordWidth; + } + pos = endPos; + } } } + flushLine(); } - if (!currentLine.empty()) - lines.push_back(currentLine); + const auto &lines = this->freeTextLayoutCacheLines; const int rowHeight = std::max(8, FONT_HEIGHT_SMALL - 3); const int viewportTop = inputY; @@ -1332,7 +1348,8 @@ void CannedMessageModule::drawFreeTextScreen(OLEDDisplay *display, OLEDDisplayUi // Reserve enough space for the completion row so chip/text rendering is not clipped. const int completionRowHeight = FONT_HEIGHT_SMALL + 1; - const int textViewportBottom = showCompletionRow ? std::max(viewportTop, viewportBottom - completionRowHeight) : viewportBottom; + const int textViewportBottom = + showCompletionRow ? std::max(viewportTop, viewportBottom - completionRowHeight) : viewportBottom; const int viewportHeight = std::max(1, textViewportBottom - viewportTop); const int viewportRows = std::max(1, viewportHeight / rowHeight); @@ -1397,22 +1414,35 @@ void CannedMessageModule::drawFreeTextScreen(OLEDDisplay *display, OLEDDisplayUi // Draw scrollable emote picker list. void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { +#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) + EInkDynamicDisplay *einkDisplay = static_cast(display); + einkDisplay->enableUnlimitedFastMode(); +#endif + const int headerFontHeight = FONT_HEIGHT_SMALL; const int headerMargin = 2; const int labelGap = 6; const int bitmapGapX = 4; + const int displayWidth = display->getWidth(); + const int displayHeight = display->getHeight(); - int maxEmoteHeight = 0; - for (int i = 0; i < graphics::numEmotes; ++i) { - if (graphics::emotes[i].height > maxEmoteHeight) { - maxEmoteHeight = graphics::emotes[i].height; + // Cache max row bitmap height; emote table is static in runtime. + static int cachedMaxEmoteHeight = -1; + static int cachedNumEmotes = -1; + if (cachedMaxEmoteHeight < 0 || cachedNumEmotes != graphics::numEmotes) { + cachedMaxEmoteHeight = 0; + for (int i = 0; i < graphics::numEmotes; ++i) { + if (graphics::emotes[i].height > cachedMaxEmoteHeight) { + cachedMaxEmoteHeight = graphics::emotes[i].height; + } } + cachedNumEmotes = graphics::numEmotes; } - const int rowHeight = maxEmoteHeight + 2; + const int rowHeight = cachedMaxEmoteHeight + 2; int headerY = y; int listTop = headerY + headerFontHeight + headerMargin; - int _visibleRows = (display->getHeight() - listTop - 2) / rowHeight; + int _visibleRows = (displayHeight - listTop - 2) / rowHeight; int numEmotes = graphics::numEmotes; this->visibleRows = _visibleRows; @@ -1434,8 +1464,9 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(display->getWidth() / 2, headerY, "Select Emote"); + display->drawString(displayWidth / 2, headerY, "Select Emote"); display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_MEDIUM); for (int vis = 0; vis < _visibleRows; ++vis) { int emoteIdx = topIndex + vis; @@ -1446,14 +1477,13 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla int rowY = listTop + vis * rowHeight; if (emoteIdx == emotePickerIndex) { - display->fillRect(x, rowY, display->getWidth() - 8, emote.height + 2); + display->fillRect(x, rowY, displayWidth - 8, rowHeight); display->setColor(BLACK); } int emoteY = rowY + 1; display->drawXbm(x + bitmapGapX, emoteY, emote.width, emote.height, emote.bitmap); - display->setFont(FONT_MEDIUM); int labelY = rowY + ((rowHeight - FONT_HEIGHT_MEDIUM) / 2); display->drawString(x + bitmapGapX + emote.width + labelGap, labelY, emote.label); @@ -1464,7 +1494,7 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla if (numEmotes > _visibleRows) { int scrollbarHeight = _visibleRows * rowHeight; - int scrollTrackX = display->getWidth() - 6; + int scrollTrackX = displayWidth - 6; display->drawRect(scrollTrackX, listTop, 4, scrollbarHeight); int scrollBarLen = std::max(6, (scrollbarHeight * _visibleRows) / numEmotes); int scrollBarPos = listTop + (scrollbarHeight * topIndex) / numEmotes;