diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 25a70f16d..9fb1ed140 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -12,6 +12,7 @@ #include "graphics/TimeFormatters.h" #include "graphics/images.h" #include "main.h" +#include "modules/CannedMessageModule.h" #include "target_specific.h" #include #include @@ -1422,7 +1423,9 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing; const int xStart = (SCREEN_WIDTH - totalWidth) / 2; - bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS; + // Suppress nav overlay while canned-message freetext composer is active. + const bool hideForFreeText = (cannedMessageModule && cannedMessageModule->isFreeTextUIActive()); + bool navBarVisible = !hideForFreeText && (millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS); int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT; #if defined(USE_EINK) @@ -1452,6 +1455,10 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta navBarPrevVisible = navBarVisible; #endif + if (!navBarVisible) { + return; + } + // Pre-calculate bounding rect const int rectX = xStart - 2 - bigOffset; const int rectWidth = totalWidth + 4 + (bigOffset * 2); diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index 6d1658b4f..0ce287fe4 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -72,6 +72,7 @@ class CannedMessageModule : public SinglePortModule, public Observable viewportBottom) { + const int completionRowY = rowY; + if (completionRowY < 0 || completionRowY >= display->getHeight()) { return; } const int spaceWidth = display->getStringWidth(" "); - const int separatorWidth = spaceWidth * 2; + const int separatorWidth = spaceWidth; const int viewportWidth = display->getWidth(); + const int chipPaddingX = 3; + const int chipRadius = 2; + const int chipHeight = FONT_HEIGHT_SMALL; + + // Draw a filled rounded rectangle using rects + circles (works across OLED/EInk backends). + auto drawRoundedFill = [display](int x0, int y0, int w0, int h0, int radius) { + if (w0 <= 0 || h0 <= 0) { + return; + } + + int r = std::max(0, radius); + r = std::min(r, std::min(w0 / 2, h0 / 2)); + if (r == 0) { + display->fillRect(x0, y0, w0, h0); + return; + } + + const int centerW = w0 - (r * 2); + const int sideH = h0 - (r * 2); + if (centerW > 0) { + display->fillRect(x0 + r, y0, centerW, h0); + } + if (sideH > 0) { + display->fillRect(x0, y0 + r, r, sideH); + display->fillRect(x0 + w0 - r, y0 + r, r, sideH); + } + display->fillCircle(x0 + r, y0 + r, r); + display->fillCircle(x0 + w0 - r - 1, y0 + r, r); + display->fillCircle(x0 + r, y0 + h0 - r - 1, r); + display->fillCircle(x0 + w0 - r - 1, y0 + h0 - r - 1, r); + }; struct ChoiceLayout { uint8_t idx; @@ -631,13 +661,14 @@ void CannedMessageModule::drawFreeTextCompletionRow(OLEDDisplay *display, int16_ } int tokenWidth = display->getStringWidth(candidate); - choices.push_back({i, candidate, runningX, tokenWidth}); + int chipWidth = tokenWidth + (chipPaddingX * 2); + choices.push_back({i, candidate, runningX, chipWidth}); if (i == this->freeTextCompletionIndex) { selectedStart = runningX; - selectedEnd = runningX + tokenWidth; + selectedEnd = runningX + chipWidth; } - runningX += tokenWidth; + runningX += chipWidth; hasChoices = true; } @@ -653,26 +684,36 @@ void CannedMessageModule::drawFreeTextCompletionRow(OLEDDisplay *display, int16_ } } + // Center the whole row when all chips fit on screen; otherwise keep scroll behavior. + const int centeredOffsetX = (runningX < viewportWidth) ? ((viewportWidth - runningX) / 2) : 0; + const int drawBaseX = x + centeredOffsetX; + for (const auto &choice : choices) { - int drawX = x + (choice.startX - choiceScrollX); - if ((drawX + choice.width) < x || drawX > (x + viewportWidth)) { + int boxX = drawBaseX + (choice.startX - choiceScrollX); + if ((boxX + choice.width) < x || boxX > (x + viewportWidth)) { continue; } + const int textX = boxX + chipPaddingX; if (choice.idx == this->freeTextCompletionIndex) { -#ifdef USE_EINK - display->drawRect(drawX - 1, completionRowY, choice.width + 2, FONT_HEIGHT_SMALL - 1); - display->drawString(drawX, completionRowY, choice.word); -#else - display->fillRect(drawX - 1, completionRowY, choice.width + 2, FONT_HEIGHT_SMALL); - display->setColor(BLACK); - display->drawString(drawX, completionRowY, choice.word); + // Selected completion: filled rounded chip. display->setColor(WHITE); -#endif + drawRoundedFill(boxX, completionRowY, choice.width, chipHeight, chipRadius); + display->setColor(BLACK); + display->drawString(textX, completionRowY, choice.word); } else { - display->drawString(drawX, completionRowY, choice.word); + // Unselected completion: hollow rounded chip. + display->setColor(WHITE); + drawRoundedFill(boxX, completionRowY, choice.width, chipHeight, chipRadius); + if (choice.width > 2 && chipHeight > 2) { + display->setColor(BLACK); + drawRoundedFill(boxX + 1, completionRowY + 1, choice.width - 2, chipHeight - 2, chipRadius - 1); + } + display->setColor(WHITE); + display->drawString(textX, completionRowY, choice.word); } } + display->setColor(WHITE); } // Insert visual cursor (and optional completion suffix hint) into rendered text. @@ -1174,7 +1215,14 @@ void CannedMessageModule::drawFreeTextScreen(OLEDDisplay *display, OLEDDisplayUi const int rowHeight = std::max(8, FONT_HEIGHT_SMALL - 3); const int viewportTop = inputY; const int viewportBottom = y + display->getHeight(); - const int viewportHeight = std::max(1, viewportBottom - viewportTop); + String completionPrefix = this->getFreeTextPrefix(); + const bool showCompletionRow = + (this->cursor == this->freetext.length() && this->freeTextCompletionCount > 1 && completionPrefix.length() >= 2); + + // 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 viewportHeight = std::max(1, textViewportBottom - viewportTop); const int viewportRows = std::max(1, viewportHeight / rowHeight); int cursorRow = static_cast(lines.size()) - 1; @@ -1197,12 +1245,9 @@ void CannedMessageModule::drawFreeTextScreen(OLEDDisplay *display, OLEDDisplayUi } } - String completionPrefix = this->getFreeTextPrefix(); - const bool showCompletionRow = - (this->cursor == this->freetext.length() && this->freeTextCompletionCount > 1 && completionPrefix.length() >= 2); - const int totalRows = static_cast(lines.size()) + (showCompletionRow ? 1 : 0); + const int totalRows = static_cast(lines.size()); int scrollRows = std::max(0, totalRows - viewportRows); - int targetRow = cursorRow + (showCompletionRow ? 1 : 0); + int targetRow = cursorRow; if (targetRow < scrollRows) { scrollRows = targetRow; } @@ -1216,7 +1261,7 @@ void CannedMessageModule::drawFreeTextScreen(OLEDDisplay *display, OLEDDisplayUi // Draw wrapped text rows with vertical scrolling. for (int rowIdx = 0; rowIdx < static_cast(lines.size()); ++rowIdx) { int yLine = viewportTop + ((rowIdx - scrollRows) * rowHeight); - if (yLine < viewportTop || (yLine + rowHeight) > (viewportBottom + rowHeight)) { + if (yLine < viewportTop || yLine >= textViewportBottom) { continue; } @@ -1232,8 +1277,7 @@ void CannedMessageModule::drawFreeTextScreen(OLEDDisplay *display, OLEDDisplayUi } if (showCompletionRow) { - this->drawFreeTextCompletionRow(display, x, viewportTop, viewportBottom, rowHeight, static_cast(lines.size()), - scrollRows, completionPrefix); + this->drawFreeTextCompletionRow(display, x, viewportBottom - completionRowHeight, completionPrefix); } } #endif