Optimization

This commit is contained in:
HarukiToreda
2026-02-19 17:09:28 -05:00
parent 951b812693
commit f6bf33d61d
4 changed files with 270 additions and 157 deletions
+18 -21
View File
@@ -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);
+7
View File
@@ -2,6 +2,8 @@
#if HAS_SCREEN
#include "ProtobufModule.h"
#include "input/InputBroker.h"
#include <utility>
#include <vector>
// ============================
// Enums & Defines
@@ -155,6 +157,10 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
uint8_t freeTextCompletionCount = 0;
uint8_t freeTextCompletionIndex = 0;
bool freeTextCompletionSuppressed = false;
// Cached wrapped token lines for freetext rendering (rebuilt only when content/width changes).
String freeTextLayoutCacheText;
int freeTextLayoutCacheWidth = -1;
std::vector<std::vector<std::pair<bool, String>>> 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<const UIF
bool handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect);
bool handleFreeTextInput(const InputEvent *event);
bool tryStartFreeTextFromInactive(const InputEvent *event);
void clearComposeState();
int32_t runFreeTextState(UIFrameEvent &e);
String getFreeTextPrefix() const;
void updateFreeTextCompletion();
+137 -58
View File
@@ -29,6 +29,106 @@ extern int bannerSignalBars;
// Tracks whether destination-picker cancel/select should return to canned list vs freetext.
static bool returnToCannedList = false;
namespace
{
// ASCII lowercase helper keeps filtering deterministic and avoids locale overhead.
char toLowerAscii(char c)
{
if (c >= 'A' && c <= 'Z') {
return static_cast<char>(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<String> 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<int> 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<std::pair<bool, String>> 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
+108 -78
View File
@@ -20,6 +20,7 @@ static constexpr int32_t kInactivateAfterMs = 20000;
std::vector<std::pair<bool, String>> freeTextModule::tokenizeMessageWithEmotes(const char *msg)
{
std::vector<std::pair<bool, String>> 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<ChoiceLayout> 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<unsigned int>(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<std::pair<bool, String>> 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<std::vector<std::pair<bool, String>>> lines;
std::vector<std::pair<bool, String>> 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<std::pair<bool, String>> tokens = freeTextModule::tokenizeMessageWithEmotes(msg);
std::vector<std::pair<bool, String>> currentLine;
currentLine.reserve(8);
int lineWidth = 0;
// Commit the current line and reset wrapping state.
auto flushLine = [this, &currentLine, &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<int>(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<int>(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<EInkDynamicDisplay *>(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;