2.7 Miscellaneous Fixes - Week 1 (#7102)

* Update Favorite Node Message Options to unify against other screens

* Rebuild Horizontal Battery, Resolve overlap concerns

* Update positioning on Message frame and fix drawCommonHeader overlay

* Beginnings of creating isHighResolution bool

* Fixup determineResolution()

* Implement isHighResolution in place of SCREEN_WIDTH > 128 checks

* Line Spacing bound to isHighResolution

* Analog Clock for all

* Add AM/PM to Analog Clock if isHighResolution and not TWatch

* Simple Menu Queue, and add time menu

* Fix prompt string for 12/24 hour picker

* More menu banners into functions

* Fix Action Menu on Home frame

* Correct pop-up calculation size and continue to leverage isHighResolution

* Move menu bits to MenuHandler

* Plumb in the digital/analog picker

* Correct Clock Face Picker title

* Clock picker fixes

* Migrate the rest of the menus to MenuHandler.*

* Add compass menu and needle point option

* Minor fix for compass point menu

* Correct Home menu into typical format

* Fix emoji bounce, overlap, and missing commonHeader

* Sanitize long_names and removed unused variables

* Slightly better sanitizeString variation

* Resolved apostrophe being shown as upside down question mark

* Gotta keep height and width in expected order

* Remove Second Hand for Analog Clock on EInk displays

* Fix Clock menu option decision tree

* Improvements to Eink Navigation

* Pause Banner for Eink moved to bottom

* Updated working for 12-/24-hour menu and Added US/Arizona to timezone picker

* Add Adhoc Ping and resolve error with std::string sanitized

* Hide quick toggle as option is available within Action Menu, commented out for the moment

* Remove old battery icon and option, use drawCommonHeader throughout, re-add battery to Clock frames

* fix misc build warnings. NFC

* Update Analog Clock on EInk to show more digits

* Establish Action Menu on all node list screens, add NodeDB reset (with confirmation) option

* Add Toggle Backlight for EInk Displays

* Suppress action screen Full refresh for Eink

* Adjust drawBluetoothConnectedIcon on TWatch

* Maintain clock frame when switching between Clock Faces

* Move modules beyond the clock in navigation

* addressed the conflicts, and changed target branch to 2.7-MiscFixes-Week1

* cleanup, cheers

* Add AM/PM to low resolution clock also

* Small adjustments to AM/PM replacement across various devices

* Resolve dangling pointer issues with sanitize code

* Update comments for Screen.cpp related to module load change

* Trunk runs

* Update message caching to correct aged timestamp

* Menu wording adjustments

* Time Format wording

* Use all the rows on EInk since with autohide the navigation bar

* Finalize Time Format picker word change

* Retired drawFunctionOverlay code

No longer being used

* Actually honor the points-north setting

* Trunk

* Compressed action list

* Update no-op showOverlayBanner function

* trunk

* Correct T_Watch_S3 specific line

* Autosized Action menu per screen

* Finalize Autosized Action menu per screen

* Unify Message Titles

* Reorder Timezones to match expectations

* Adjust text location for pop-ups

* Revert "Actually honor the points-north setting"

This reverts commit 20988aa4fa.

* Make NodeDB sort its internal vector when lastheard is updated. Don't sort in NodeListRenderer

* Update src/graphics/draw/NodeListRenderer.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/mesh/NodeDB.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Pass by reference -- Thanks Copilot!

* Throttle sorting just a touch

* Check more carefully for own node

* Eliminate some now-unneeded sorting

* Move function after include

* Putting Modules back to position 0 and some trunk checks found

* Add Scrollbar for Action menus

* Second attempt to move modules down the navigation bar

* Continue effort of moving modules in the navigation

* Canned Messages tweak

* Replicate Function + Space through the Menu System

* Move init button parameters into config struct (#7145)

* Remove bundling of web-ui from ESP32 devices (#7143)

* Fixed triple click GPS toggle bungle

* Move init button parameters into config struct

* Reapply "Actually honor the points-north setting"

This reverts commit 42c1967e7b.

* Actually do compass pointings correctly

* Tweak to node bearings

* Menu wording tweaks

* Get the compass_north_top logic right

* Don't jump frames after setting Compass

* Get rid of the extra bearingTo functions

* Don't blink Mail on EInk Clock Screens

* Actually set lat and long

* Calibrate

* Convert Radians to Degrees

* More degree vs radians fixes

* De-duplicate draw arrow function

* Don't advertise compass calibration without an accell thread.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: csrutil <keming.cao@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Jason P
2025-06-26 22:11:20 -05:00
committed by GitHub
parent 18fbc2149d
commit 29e7a71c97
36 changed files with 1429 additions and 868 deletions

View File

@@ -18,6 +18,32 @@
#include <RTC.h>
#include <cstring>
bool isAllowedPunctuation(char c)
{
const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ ";
return allowed.find(c) != std::string::npos;
}
std::string sanitizeString(const std::string &input)
{
std::string output;
bool inReplacement = false;
for (char c : input) {
if (std::isalnum(static_cast<unsigned char>(c)) || isAllowedPunctuation(c)) {
output += c;
inReplacement = false;
} else {
if (!inReplacement) {
output += 0xbf; // ISO-8859-1 for inverted question mark
inReplacement = true;
}
}
}
return output;
}
#if !MESHTASTIC_EXCLUDE_GPS
// External variables
@@ -38,7 +64,7 @@ NodeNum UIRenderer::currentFavoriteNodeNum = 0;
void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps)
{
// Draw satellite image
if (SCREEN_WIDTH > 128) {
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite);
@@ -58,7 +84,7 @@ void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const mesht
} else {
snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites());
}
if (SCREEN_WIDTH > 128) {
if (isHighResolution) {
display->drawString(x + 18, y, textString);
} else {
display->drawString(x + 11, y, textString);
@@ -163,46 +189,6 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y,
}
}
void UIRenderer::drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer,
const meshtastic::PowerStatus *powerStatus)
{
static const uint8_t powerBar[3] = {0x81, 0xBD, 0xBD};
static const uint8_t lightning[8] = {0xA1, 0xA1, 0xA5, 0xAD, 0xB5, 0xA5, 0x85, 0x85};
// Clear the bar area inside the battery image
for (int i = 1; i < 14; i++) {
imgBuffer[i] = 0x81;
}
// Fill with lightning or power bars
if (powerStatus->getIsCharging()) {
memcpy(imgBuffer + 3, lightning, 8);
} else {
for (int i = 0; i < 4; i++) {
if (powerStatus->getBatteryChargePercent() >= 25 * i)
memcpy(imgBuffer + 1 + (i * 3), powerBar, 3);
}
}
// Slightly more conservative scaling based on screen width
int scale = 1;
if (SCREEN_WIDTH >= 200)
scale = 2;
if (SCREEN_WIDTH >= 300)
scale = 2; // Do NOT go higher than 2
// Draw scaled battery image (16 columns × 8 rows)
for (int col = 0; col < 16; col++) {
uint8_t colBits = imgBuffer[col];
for (int row = 0; row < 8; row++) {
if (colBits & (1 << row)) {
display->fillRect(x + col * scale, y + row * scale, scale, scale);
}
}
}
}
// Draw nodes status
void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset,
bool show_total, String additional_words)
@@ -221,19 +207,19 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \
!defined(DISPLAY_FORCE_SMALL_FONTS)
if (SCREEN_WIDTH > 128) {
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display);
} else {
display->drawFastImage(x, y + 3, 8, 8, imgUser);
}
#else
if (SCREEN_WIDTH > 128) {
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display);
} else {
display->drawFastImage(x, y + 1, 8, 8, imgUser);
}
#endif
int string_offset = (SCREEN_WIDTH > 128) ? 9 : 0;
int string_offset = (isHighResolution) ? 9 : 0;
display->drawString(x + 10 + string_offset, y - 2, usersString);
}
@@ -293,12 +279,14 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
// List of available macro Y positions in order, from top to bottom.
int line = 1; // which slot to use next
std::string usernameStr;
// === 1. Long Name (always try to show first) ===
const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr;
if (username && line < 5) {
if (username) {
usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case
// Print node's long name (e.g. "Backpack Node")
display->drawString(x, getTextPositions(display)[line++], username);
display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str());
}
// === 2. Signal and Hops (combined on one line, if available) ===
@@ -456,8 +444,11 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
*/
float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
if (!config.display.compass_north_top)
if (screen->ignoreCompass) {
myHeading = 0;
} else {
bearing -= myHeading;
}
display->drawCircle(compassX, compassY, compassRadius);
CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
@@ -476,7 +467,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
const int margin = 4;
// --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) -----------
#if defined(USE_EINK)
const int iconSize = (SCREEN_WIDTH > 128) ? 16 : 8;
const int iconSize = (isHighResolution) ? 16 : 8;
const int navBarHeight = iconSize + 6;
#else
const int navBarHeight = 0;
@@ -497,8 +488,11 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
int compassY = yBelowContent + availableHeight / 2;
const auto &op = ourNode->position;
float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
float myHeading = 0;
if (!screen->ignoreCompass) {
myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
}
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
const auto &p = node->position;
@@ -507,7 +501,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
*/
float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
if (!config.display.compass_north_top)
if (!screen->ignoreCompass)
bearing -= myHeading;
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing);
@@ -570,15 +564,15 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
} else {
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
}
int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1;
if (SCREEN_WIDTH > 128) {
int yOffset = (isHighResolution) ? 3 : 1;
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite);
}
int xOffset = (SCREEN_WIDTH > 128) ? 6 : 0;
int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine);
} else {
UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus);
@@ -602,17 +596,17 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
char chUtilPercentage[10];
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
int chUtil_x = (SCREEN_WIDTH > 128) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
int chUtil_y = getTextPositions(display)[line] + 3;
int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50;
int chutil_bar_width = (isHighResolution) ? 100 : 50;
if (!config.bluetooth.enabled) {
chutil_bar_width = (SCREEN_WIDTH > 128) ? 80 : 40;
chutil_bar_width = (isHighResolution) ? 80 : 40;
}
int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7;
int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3;
int chutil_bar_height = (isHighResolution) ? 12 : 7;
int extraoffset = (isHighResolution) ? 6 : 3;
if (!config.bluetooth.enabled) {
extraoffset = (SCREEN_WIDTH > 128) ? 6 : 1;
extraoffset = (isHighResolution) ? 6 : 1;
}
int chutil_percent = airTime->channelUtilizationPercent();
@@ -672,21 +666,20 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
// === Fourth & Fifth Rows: Node Identity ===
int textWidth = 0;
int nameX = 0;
int yOffset = (SCREEN_WIDTH > 128) ? 0 : 5;
int yOffset = (isHighResolution) ? 0 : 5;
const char *longName = nullptr;
std::string longNameStr;
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) {
longName = ourNode->user.long_name;
longNameStr = sanitizeString(ourNode->user.long_name);
}
uint8_t dmac[6];
char shortnameble[35];
getMacAddr(dmac);
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
snprintf(shortnameble, sizeof(shortnameble), "%s",
graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
char combinedName[50];
snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortnameble);
snprintf(combinedName, sizeof(combinedName), "%s (%s)", longNameStr.empty() ? "" : longNameStr.c_str(), shortnameble);
if (SCREEN_WIDTH - (display->getStringWidth(longName) + display->getStringWidth(shortnameble)) > 10) {
size_t len = strlen(combinedName);
if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) {
@@ -700,7 +693,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
// === LongName Centered ===
textWidth = display->getStringWidth(longName);
nameX = (SCREEN_WIDTH - textWidth) / 2;
display->drawString(nameX, getTextPositions(display)[line++], longName);
display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str());
// === ShortName Centered ===
textWidth = display->getStringWidth(shortnameble);
@@ -808,44 +801,42 @@ void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState
{
LOG_DEBUG("Draw screensaver overlay");
EINK_ADD_FRAMEFLAG(display, COSMETIC); // Take the opportunity for a full-refresh
EINK_ADD_FRAMEFLAG(display, COSMETIC); // Full refresh for screensaver
// Config
display->setFont(FONT_SMALL);
display->setTextAlignment(TEXT_ALIGN_LEFT);
const char *pauseText = "Screen Paused";
const char *idText = owner.short_name;
const bool useId = haveGlyphs(idText); // This bool is used to hide the idText box if we can't render the short name
constexpr uint16_t padding = 5;
const bool useId = haveGlyphs(idText);
constexpr uint8_t padding = 2;
constexpr uint8_t dividerGap = 1;
constexpr uint8_t imprecision = 5; // How far the box origins can drift from center. Combat burn-in.
// Dimensions
const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); // "true": handle utf8 chars
// Text widths
const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true);
const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText));
const uint16_t boxWidth = padding + (useId ? idTextWidth + padding + padding : 0) + pauseTextWidth + padding;
const uint16_t boxHeight = padding + FONT_HEIGHT_SMALL + padding;
const uint16_t boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding;
const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2);
// Position
const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2) + random(-imprecision, imprecision + 1);
// const int16_t boxRight = boxLeft + boxWidth - 1;
const int16_t boxTop = (display->height() / 2) - (boxHeight / 2 + random(-imprecision, imprecision + 1));
const int16_t boxBottom = boxTop + boxHeight - 1;
// Flush with bottom
const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
const int16_t boxTop = display->height() - boxHeight;
const int16_t boxBottom = display->height() - 1;
const int16_t idTextLeft = boxLeft + padding;
const int16_t idTextTop = boxTop + padding;
const int16_t pauseTextLeft = boxLeft + (useId ? padding + idTextWidth + padding : 0) + padding;
const int16_t pauseTextLeft = boxLeft + (useId ? idTextWidth + (padding * 2) : 0) + padding;
const int16_t pauseTextTop = boxTop + padding;
const int16_t dividerX = boxLeft + padding + idTextWidth + padding;
const int16_t dividerTop = boxTop + 1 + dividerGap;
const int16_t dividerBottom = boxBottom - 1 - dividerGap;
const int16_t dividerTop = boxTop + dividerGap;
const int16_t dividerBottom = boxBottom - dividerGap;
// Draw: box
display->setColor(EINK_WHITE);
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Clear a slightly oversized area for the box
display->fillRect(boxLeft, boxTop, boxWidth, boxHeight);
display->setColor(EINK_BLACK);
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
// Draw: Text
// Draw: text
if (useId)
display->drawString(idTextLeft, idTextTop, idText);
display->drawString(pauseTextLeft, pauseTextTop, pauseText);
@@ -920,15 +911,15 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
} else {
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
}
int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1;
if (SCREEN_WIDTH > 128) {
int yOffset = (isHighResolution) ? 3 : 1;
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
imgSatellite_height, imgSatellite, display);
} else {
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
imgSatellite);
}
int xOffset = (SCREEN_WIDTH > 128) ? 6 : 0;
int xOffset = (isHighResolution) ? 6 : 0;
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
} else {
UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus);
@@ -941,15 +932,18 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
int32_t(gpsStatus->getAltitude()));
// === Determine Compass Heading ===
float heading;
float heading = 0;
bool validHeading = false;
if (screen->hasHeading()) {
heading = radians(screen->getHeading());
if (screen->ignoreCompass) {
validHeading = true;
} else {
heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7);
validHeading = !isnan(heading);
if (screen->hasHeading()) {
heading = radians(screen->getHeading());
validHeading = true;
} else {
heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7);
validHeading = !isnan(heading);
}
}
// If GPS is off, no need to display these parts
@@ -1005,7 +999,9 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
display->drawCircle(compassX, compassY, compassRadius);
// "N" label
float northAngle = -heading;
float northAngle = 0;
if (!config.display.compass_north_top)
northAngle = -heading;
float radius = compassRadius;
int16_t nX = compassX + (radius - 1) * sin(northAngle);
int16_t nY = compassY - (radius - 1) * cos(northAngle);
@@ -1046,7 +1042,9 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
display->drawCircle(compassX, compassY, compassRadius);
// "N" label
float northAngle = -heading;
float northAngle = 0;
if (!config.display.compass_north_top)
northAngle = -heading;
float radius = compassRadius;
int16_t nX = compassX + (radius - 1) * sin(northAngle);
int16_t nY = compassY - (radius - 1) * cos(northAngle);
@@ -1114,18 +1112,6 @@ void UIRenderer::drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *sta
#endif
// Function overlay for showing mute/buzzer modifiers etc.
void UIRenderer::drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
{
// LOG_DEBUG("Draw function overlay");
if (functionSymbol.begin() != functionSymbol.end()) {
char buf[64];
display->setFont(FONT_SMALL);
snprintf(buf, sizeof(buf), "%s", functionSymbolString.c_str());
display->drawString(SCREEN_WIDTH - display->getStringWidth(buf), SCREEN_HEIGHT - FONT_HEIGHT_SMALL, buf);
}
}
// Navigation bar overlay implementation
static int8_t lastFrameIndex = -1;
static uint32_t lastFrameChangeTime = 0;
@@ -1141,10 +1127,9 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
lastFrameChangeTime = millis();
}
const bool useBigIcons = (SCREEN_WIDTH > 128);
const int iconSize = useBigIcons ? 16 : 8;
const int spacing = useBigIcons ? 8 : 4;
const int bigOffset = useBigIcons ? 1 : 0;
const int iconSize = isHighResolution ? 16 : 8;
const int spacing = isHighResolution ? 8 : 4;
const int bigOffset = isHighResolution ? 1 : 0;
const size_t totalIcons = screen->indicatorIcons.size();
if (totalIcons == 0)
@@ -1158,14 +1143,35 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing;
const int xStart = (SCREEN_WIDTH - totalWidth) / 2;
// Only show bar briefly after switching frames (unless on E-Ink)
// Only show bar briefly after switching frames
static uint32_t navBarLastShown = 0;
static bool cosmeticRefreshDone = false;
bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS;
int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT;
#if defined(USE_EINK)
int y = SCREEN_HEIGHT - iconSize - 1;
#else
int y = SCREEN_HEIGHT - iconSize - 1;
if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) {
y = SCREEN_HEIGHT;
static bool navBarPrevVisible = false;
if (navBarVisible && !navBarPrevVisible) {
EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when showing nav bar
cosmeticRefreshDone = false;
navBarLastShown = millis();
}
if (!navBarVisible && navBarPrevVisible) {
EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when hiding nav bar
navBarLastShown = millis(); // Mark when it disappeared
}
if (!navBarVisible && navBarLastShown != 0 && !cosmeticRefreshDone) {
if (millis() - navBarLastShown > 10000) { // 10s after hidden
EINK_ADD_FRAMEFLAG(display, COSMETIC); // One-time ghost cleanup
cosmeticRefreshDone = true;
}
}
navBarPrevVisible = navBarVisible;
#endif
// Pre-calculate bounding rect
@@ -1191,7 +1197,7 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
display->setColor(BLACK);
}
if (useBigIcons) {
if (isHighResolution) {
NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display);
} else {
display->drawXbm(x, y, iconSize, iconSize, icon);