Improved message screen with scrolling

This commit is contained in:
HarukiToreda 2025-04-04 01:13:15 -04:00
parent 7856e069a5
commit 2711c53b5f

View File

@ -980,146 +980,214 @@ bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int
validCached = true;
return validCached;
}
/// Draw the last text message we received
static void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r)
{
// the max length of this buffer is much longer than we can possibly print
static char tempBuf[237];
display->fillRect(x + r, y, w - 2 * r, h);
display->fillRect(x, y + r, r, h - 2 * r);
display->fillRect(x + w - r, y + r, r, h - 2 * r);
display->fillCircle(x + r + 1, y + r, r);
display->fillCircle(x + w - r - 1, y + r, r);
display->fillCircle(x + r + 1, y + h - r - 1, r);
display->fillCircle(x + w - r - 1, y + h - r - 1, r);
}
// ****************************
// * Tex Message Screen *
// ****************************
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
// LOG_DEBUG("Draw text message from 0x%x: %s", mp.from,
// mp.decoded.variant.data.decoded.bytes);
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
// Demo for drawStringMaxWidth:
// with the third parameter you can define the width after which words will
// be wrapped. Currently only spaces and "-" are allowed for wrapping
// === Setup display formatting ===
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
display->setColor(BLACK);
}
// For time delta
uint32_t seconds = sinceReceived(&mp);
uint32_t minutes = seconds / 60;
uint32_t hours = minutes / 60;
uint32_t days = hours / 24;
const int screenWidth = display->getWidth();
const int screenHeight = display->getHeight();
const int navHeight = FONT_HEIGHT_SMALL; // space reserved at bottom
const int scrollBottom = screenHeight - navHeight;
const int usableHeight = scrollBottom;
const int textWidth = screenWidth;
const int cornerRadius = 2;
// For timestamp
bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
bool isBold = config.display.heading_bold;
// === Construct Header String ===
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp));
char headerStr[80];
const char *sender = (node && node->has_user) ? node->user.short_name : "???";
uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
uint8_t timestampHours, timestampMinutes;
int32_t daysAgo;
bool useTimestamp = deltaToTimestamp(seconds, &timestampHours, &timestampMinutes, &daysAgo);
char from_string[75];
if (useTimestamp) {
std::string prefix = (daysAgo == 1 && display->width() >= 200) ? "Yesterday" : "At";
if (useTimestamp && minutes >= 15 && daysAgo == 0) {
std::string prefix = (daysAgo == 1 && screenWidth >= 200) ? "Yesterday" : "At";
std::string meridiem = "AM";
if (config.display.use_12h_clock) {
std::string meridiem = "AM";
if (timestampHours >= 12) {
meridiem = "PM";
}
if (timestampHours > 12) {
timestampHours -= 12;
}
if (timestampHours == 00) {
timestampHours = 12;
}
snprintf(from_string, sizeof(from_string), "%s %d:%02d%s from", prefix.c_str(), timestampHours, timestampMinutes,
meridiem.c_str());
if (timestampHours >= 12) meridiem = "PM";
if (timestampHours > 12) timestampHours -= 12;
if (timestampHours == 0) timestampHours = 12;
snprintf(headerStr, sizeof(headerStr), "%s %d:%02d%s from %s", prefix.c_str(), timestampHours, timestampMinutes, meridiem.c_str(), sender);
} else {
snprintf(from_string, sizeof(from_string), "%s %d:%02d from", prefix.c_str(), timestampHours, timestampMinutes);
snprintf(headerStr, sizeof(headerStr), "%s %d:%02d from %s", prefix.c_str(), timestampHours, timestampMinutes, sender);
}
}
// If bold, draw twice, shifting right by one pixel
for (uint8_t xOff = 0; xOff <= (config.display.heading_bold ? 1 : 0); xOff++) {
// Show a timestamp if received today, but longer than 15 minutes ago
if (useTimestamp && minutes >= 15 && daysAgo == 0) {
display->drawStringf(xOff + x, 0 + y, tempBuf, "%s %s", from_string,
(node && node->has_user) ? node->user.short_name : "???");
} else {
display->drawStringf(xOff + x, 0 + y, tempBuf, "%s ago from %s",
screen->drawTimeDelta(days, hours, minutes, seconds).c_str(),
(node && node->has_user) ? node->user.short_name : "???");
}
}
display->setColor(WHITE);
#ifndef EXCLUDE_EMOJI
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
if (strcmp(msg, "\U0001F44D") == 0) {
display->drawXbm(x + (SCREEN_WIDTH - thumbs_width) / 2,
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - thumbs_height) / 2 + 2 + 5, thumbs_width, thumbs_height,
thumbup);
} else if (strcmp(msg, "\U0001F44E") == 0) {
display->drawXbm(x + (SCREEN_WIDTH - thumbs_width) / 2,
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - thumbs_height) / 2 + 2 + 5, thumbs_width, thumbs_height,
thumbdown);
} else if (strcmp(msg, "\U0001F60A") == 0 || strcmp(msg, "\U0001F600") == 0 || strcmp(msg, "\U0001F642") == 0 ||
strcmp(msg, "\U0001F609") == 0 ||
strcmp(msg, "\U0001F601") == 0) { // matches 5 different common smileys, so that the phone user doesn't have to
// remember which one is compatible
display->drawXbm(x + (SCREEN_WIDTH - smiley_width) / 2,
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - smiley_height) / 2 + 2 + 5, smiley_width, smiley_height,
smiley);
} else if (strcmp(msg, "") == 0) {
display->drawXbm(x + (SCREEN_WIDTH - question_width) / 2,
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - question_height) / 2 + 2 + 5, question_width, question_height,
question);
} else if (strcmp(msg, "‼️") == 0) {
display->drawXbm(x + (SCREEN_WIDTH - bang_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - bang_height) / 2 + 2 + 5,
bang_width, bang_height, bang);
} else if (strcmp(msg, "\U0001F4A9") == 0) {
display->drawXbm(x + (SCREEN_WIDTH - poo_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - poo_height) / 2 + 2 + 5,
poo_width, poo_height, poo);
} else if (strcmp(msg, "\U0001F923") == 0) {
display->drawXbm(x + (SCREEN_WIDTH - haha_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - haha_height) / 2 + 2 + 5,
haha_width, haha_height, haha);
} else if (strcmp(msg, "\U0001F44B") == 0) {
display->drawXbm(x + (SCREEN_WIDTH - wave_icon_width) / 2,
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - wave_icon_height) / 2 + 2 + 5, wave_icon_width,
wave_icon_height, wave_icon);
} else if (strcmp(msg, "\U0001F920") == 0) {
display->drawXbm(x + (SCREEN_WIDTH - cowboy_width) / 2,
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - cowboy_height) / 2 + 2 + 5, cowboy_width, cowboy_height,
cowboy);
} else if (strcmp(msg, "\U0001F42D") == 0) {
display->drawXbm(x + (SCREEN_WIDTH - deadmau5_width) / 2,
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - deadmau5_height) / 2 + 2 + 5, deadmau5_width, deadmau5_height,
deadmau5);
} else if (strcmp(msg, "\xE2\x98\x80\xEF\xB8\x8F") == 0) {
display->drawXbm(x + (SCREEN_WIDTH - sun_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - sun_height) / 2 + 2 + 5,
sun_width, sun_height, sun);
} else if (strcmp(msg, "\u2614") == 0) {
display->drawXbm(x + (SCREEN_WIDTH - rain_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - rain_height) / 2 + 2 + 10,
rain_width, rain_height, rain);
} else if (strcmp(msg, "☁️") == 0) {
display->drawXbm(x + (SCREEN_WIDTH - cloud_width) / 2,
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - cloud_height) / 2 + 2 + 5, cloud_width, cloud_height, cloud);
} else if (strcmp(msg, "🌫️") == 0) {
display->drawXbm(x + (SCREEN_WIDTH - fog_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - fog_height) / 2 + 2 + 5,
fog_width, fog_height, fog);
} else if (strcmp(msg, "\U0001F608") == 0) {
display->drawXbm(x + (SCREEN_WIDTH - devil_width) / 2,
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - devil_height) / 2 + 2 + 5, devil_width, devil_height, devil);
} else if (strcmp(msg, "♥️") == 0 || strcmp(msg, "\U0001F9E1") == 0 || strcmp(msg, "\U00002763") == 0 ||
strcmp(msg, "\U00002764") == 0 || strcmp(msg, "\U0001F495") == 0 || strcmp(msg, "\U0001F496") == 0 ||
strcmp(msg, "\U0001F497") == 0 || strcmp(msg, "\U0001F498") == 0) {
display->drawXbm(x + (SCREEN_WIDTH - heart_width) / 2,
y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - heart_height) / 2 + 2 + 5, heart_width, heart_height, heart);
} else {
snprintf(tempBuf, sizeof(tempBuf), "%s", mp.decoded.payload.bytes);
display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), tempBuf);
snprintf(headerStr, sizeof(headerStr), "%s ago from %s", screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), sender);
}
#ifndef EXCLUDE_EMOJI
// === Check for Emote and Draw It ===
struct Emote {
const char *code;
const uint8_t *bitmap;
int width, height;
};
const Emote emotes[] = {
{ "\U0001F44D", thumbup, thumbs_width, thumbs_height },
{ "\U0001F44E", thumbdown, thumbs_width, thumbs_height },
{ "\U0001F60A", smiley, smiley_width, smiley_height },
{ "\U0001F600", smiley, smiley_width, smiley_height },
{ "\U0001F642", smiley, smiley_width, smiley_height },
{ "\U0001F609", smiley, smiley_width, smiley_height },
{ "\U0001F601", smiley, smiley_width, smiley_height },
{ "", question, question_width, question_height },
{ "‼️", bang, bang_width, bang_height },
{ "\U0001F4A9", poo, poo_width, poo_height },
{ "\U0001F923", haha, haha_width, haha_height },
{ "\U0001F44B", wave_icon, wave_icon_width, wave_icon_height },
{ "\U0001F920", cowboy, cowboy_width, cowboy_height },
{ "\U0001F42D", deadmau5, deadmau5_width, deadmau5_height },
{ "☀️", sun, sun_width, sun_height },
{ "\xE2\x98\x80\xEF\xB8\x8F", sun, sun_width, sun_height },
{ "", rain, rain_width, rain_height },
{ "\u2614", rain, rain_width, rain_height },
{ "☁️", cloud, cloud_width, cloud_height },
{ "🌫️", fog, fog_width, fog_height },
{ "\U0001F608", devil, devil_width, devil_height },
{ "♥️", heart, heart_width, heart_height },
{ "\U0001F9E1", heart, heart_width, heart_height },
{ "\U00002763", heart, heart_width, heart_height },
{ "\U00002764", heart, heart_width, heart_height },
{ "\U0001F495", heart, heart_width, heart_height },
{ "\U0001F496", heart, heart_width, heart_height },
{ "\U0001F497", heart, heart_width, heart_height },
{ "\U0001F498", heart, heart_width, heart_height }
};
for (const Emote &e : emotes) {
if (strcmp(msg, e.code) == 0) {
// Draw header before showing emoji
if (isInverted) {
drawRoundedHighlight(display, x, 0, screenWidth, FONT_HEIGHT_SMALL - 1, cornerRadius);
display->setColor(BLACK);
display->drawString(x + 3, 0, headerStr);
if (isBold) display->drawString(x + 4, 0, headerStr);
display->setColor(WHITE);
} else {
display->drawString(x, 0, headerStr);
}
// Then draw emoji below header
display->drawXbm((screenWidth - e.width) / 2, (screenHeight - e.height) / 2 + FONT_HEIGHT_SMALL, e.width, e.height, e.bitmap);
return;
}
}
#else
snprintf(tempBuf, sizeof(tempBuf), "%s", mp.decoded.payload.bytes);
display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), tempBuf);
#endif
// === Word-wrap and build line list ===
char messageBuf[237];
snprintf(messageBuf, sizeof(messageBuf), "%s", msg);
std::vector<std::string> lines;
lines.push_back(std::string(headerStr)); // Header line is always first
std::string word, line;
int msgLen = strlen(messageBuf);
for (int i = 0; i <= msgLen; ++i) {
char ch = messageBuf[i];
if (ch == ' ' || ch == '\0') {
if (!word.empty()) {
if (display->getStringWidth((line + word).c_str()) > textWidth) {
lines.push_back(line);
line = word + ' ';
} else {
line += word + ' ';
}
word.clear();
}
if (ch == '\0' && !line.empty()) {
lines.push_back(line);
}
} else {
word += ch;
}
}
// === Scrolling logic ===
const float rowHeight = FONT_HEIGHT_SMALL - 1;
const int totalHeight = lines.size() * rowHeight;
const int scrollStop = std::max(0, totalHeight - usableHeight);
static float scrollY = 0.0f;
static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0;
static bool waitingToReset = false, scrollStarted = false;
uint32_t now = millis();
// === Smooth scrolling adjustment ===
// You can tweak this divisor to change how smooth it scrolls.
// Lower = smoother, but can feel slow.
float delta = (now - lastTime) / 400.0f;
lastTime = now;
const float scrollSpeed = 2.0f; // pixels per second
// Delay scrolling start by 2 seconds
if (scrollStartDelay == 0) scrollStartDelay = now;
if (!scrollStarted && now - scrollStartDelay > 2000) scrollStarted = true;
if (totalHeight > usableHeight) {
if (scrollStarted) {
if (!waitingToReset) {
scrollY += delta * scrollSpeed;
if (scrollY >= scrollStop) {
scrollY = scrollStop;
waitingToReset = true;
pauseStart = now;
}
} else if (now - pauseStart > 3000) {
scrollY = 0;
waitingToReset = false;
scrollStarted = false;
scrollStartDelay = now;
}
}
} else {
scrollY = 0;
}
int scrollOffset = static_cast<int>(scrollY);
int yOffset = -scrollOffset;
// === Render visible lines ===
for (size_t i = 0; i < lines.size(); ++i) {
int lineY = static_cast<int>(i * rowHeight + yOffset);
if (lineY > -rowHeight && lineY < scrollBottom) {
if (i == 0 && isInverted) {
drawRoundedHighlight(display, x, lineY, screenWidth, FONT_HEIGHT_SMALL - 1, cornerRadius);
display->setColor(BLACK);
display->drawString(x + 3, lineY, lines[i].c_str());
if (isBold) display->drawString(x + 4, lineY, lines[i].c_str());
display->setColor(WHITE);
} else {
display->drawString(x, lineY, lines[i].c_str());
}
}
}
}
/// Draw a series of fields in a column, wrapping to multiple columns if needed
@ -1605,19 +1673,6 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
// *********************************
// *Rounding Header when inverted *
// *********************************
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r)
{
// Center rectangles
display->fillRect(x + r, y, w - 2 * r, h);
display->fillRect(x, y + r, r, h - 2 * r);
display->fillRect(x + w - r, y + r, r, h - 2 * r);
// Rounded corners — visually balanced and centered
display->fillCircle(x + r + 1, y + r, r); // Top-left
display->fillCircle(x + w - r - 1, y + r, r); // Top-right
display->fillCircle(x + r + 1, y + h - r - 1, r); // Bottom-left
display->fillCircle(x + w - r - 1, y + h - r - 1, r); // Bottom-right
}
// h! Each node entry holds a reference to its info and how long ago it was heard from
struct NodeEntry {
meshtastic_NodeInfoLite *node;