mirror of
https://github.com/meshtastic/firmware.git
synced 2026-01-14 13:57:24 +00:00
Improve input box display on small screens.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "main.h"
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
@@ -70,6 +71,7 @@ void VirtualKeyboard::initializeKeyboard()
|
||||
|
||||
void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY)
|
||||
{
|
||||
// Repeat ticking is driven by NotificationRenderer once per frame
|
||||
// Base styles
|
||||
display->setColor(WHITE);
|
||||
display->setFont(FONT_SMALL);
|
||||
@@ -90,12 +92,6 @@ void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offset
|
||||
const int lastColPad = (screenW <= 128 ? 2 : 6);
|
||||
const int reservedLastColW = lastColLabelW + lastColPad; // reserved width for last column keys
|
||||
|
||||
// Dynamic key geometry
|
||||
int cellH = KEY_HEIGHT;
|
||||
if (isWide) {
|
||||
cellH = KEY_HEIGHT + 3; // slightly taller on wide screens
|
||||
}
|
||||
|
||||
// Always reserve width for the rightmost text column to avoid overlap on small screens
|
||||
int cellW = 0;
|
||||
int leftoverW = 0;
|
||||
@@ -110,6 +106,28 @@ void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offset
|
||||
leftoverW = usableW - cellW * leftCols; // distribute extra pixels over left columns (left to right)
|
||||
}
|
||||
|
||||
// Dynamic key geometry
|
||||
int cellH = KEY_HEIGHT;
|
||||
if (isWide) {
|
||||
// For wide screens (e.g., T114 240x135), prefer square keys: height equals left-column key width.
|
||||
cellH = std::max((int)KEY_HEIGHT, cellW);
|
||||
|
||||
// Guarantee at least 2 lines of input are visible by reducing cell height minimally if needed.
|
||||
// Replicate the spacing used in drawInputArea(): headerGap=1, box-to-header gap=1, gap above keyboard=1
|
||||
display->setFont(FONT_SMALL);
|
||||
const int headerHeight = headerText.empty() ? 0 : (FONT_HEIGHT_SMALL + 1);
|
||||
const int headerToBoxGap = 1;
|
||||
const int gapAboveKb = 1;
|
||||
const int minBoxHeightForTwoLines = 2 * FONT_HEIGHT_SMALL + 2; // inner 1px top/bottom
|
||||
int maxKeyboardHeight = screenH - (offsetY + headerHeight + headerToBoxGap + minBoxHeightForTwoLines + gapAboveKb);
|
||||
int maxCellHAllowed = maxKeyboardHeight / KEYBOARD_ROWS;
|
||||
if (maxCellHAllowed < (int)KEY_HEIGHT)
|
||||
maxCellHAllowed = KEY_HEIGHT;
|
||||
if (maxCellHAllowed > 0 && cellH > maxCellHAllowed) {
|
||||
cellH = maxCellHAllowed;
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard placement from bottom
|
||||
const int keyboardHeight = KEYBOARD_ROWS * cellH;
|
||||
int keyboardStartY = screenH - keyboardHeight;
|
||||
@@ -153,53 +171,151 @@ void VirtualKeyboard::draw(OLEDDisplay *display, int16_t offsetX, int16_t offset
|
||||
void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY)
|
||||
{
|
||||
display->setColor(WHITE);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
const int screenWidth = display->getWidth();
|
||||
const int lineH = FONT_HEIGHT_SMALL;
|
||||
const int screenHeight = display->getHeight();
|
||||
// Use the standard small font metrics for input box sizing (restore original size)
|
||||
const int inputLineH = FONT_HEIGHT_SMALL;
|
||||
|
||||
// Header uses the standard small (which may be larger on big screens)
|
||||
display->setFont(FONT_SMALL);
|
||||
int headerHeight = 0;
|
||||
if (!headerText.empty()) {
|
||||
// Draw header and reserve exact font height (plus a tighter gap) to maximize input area
|
||||
display->drawString(offsetX + 2, offsetY, headerText.c_str());
|
||||
headerHeight = 10;
|
||||
// On very small screens (e.g., 128x64), push the input box as close as possible to the header
|
||||
headerHeight = FONT_HEIGHT_SMALL; // no extra padding baked in
|
||||
}
|
||||
|
||||
// Input box - from below header down to just above the keyboard
|
||||
const int boxX = offsetX + 2;
|
||||
const int boxY = offsetY + headerHeight + 2;
|
||||
// Smaller gap below header on tiny screens, slightly larger otherwise
|
||||
const int gapBelowHeader = (screenHeight <= 64 ? 0 : 1);
|
||||
const int boxY = offsetY + headerHeight + gapBelowHeader;
|
||||
const int boxWidth = screenWidth - 4;
|
||||
int availableH = keyboardStartY - boxY - 2; // small gap above keyboard
|
||||
if (availableH < lineH + 2)
|
||||
availableH = lineH + 2; // ensure minimum
|
||||
const int boxHeight = availableH;
|
||||
// Ensure the box doesn't touch the keyboard: prefer a bigger guard gap on 64px screens
|
||||
int gapAboveKeyboard = (screenHeight <= 64 ? 3 : 1);
|
||||
// Minimum box height to fully contain one text line with 1px padding on top and bottom
|
||||
const int minBoxHeight = inputLineH + 2;
|
||||
int availableH = keyboardStartY - boxY - gapAboveKeyboard; // initial available height
|
||||
if (screenHeight <= 64 && availableH < minBoxHeight) {
|
||||
// Try to grow the box by reducing the gap above keyboard, but keep at least 1px separation
|
||||
int need = minBoxHeight - availableH;
|
||||
int canReduce = gapAboveKeyboard - 1;
|
||||
int reduce = std::min(need, canReduce);
|
||||
if (reduce > 0) {
|
||||
gapAboveKeyboard -= reduce;
|
||||
availableH = keyboardStartY - boxY - gapAboveKeyboard;
|
||||
}
|
||||
}
|
||||
int boxHeight;
|
||||
if (screenHeight <= 64) {
|
||||
// On tiny screens, enforce at least one text line + 2px padding when possible
|
||||
if (availableH >= minBoxHeight) {
|
||||
boxHeight = availableH; // maximize
|
||||
} else {
|
||||
// If still not enough space, use whatever is available but keep >=1px
|
||||
boxHeight = std::max(1, availableH);
|
||||
}
|
||||
} else {
|
||||
if (availableH < inputLineH + 2)
|
||||
availableH = inputLineH + 2; // ensure minimum readability on larger screens
|
||||
boxHeight = availableH;
|
||||
}
|
||||
|
||||
// Draw box border
|
||||
display->drawRect(boxX, boxY, boxWidth, boxHeight);
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
// Text rendering: multi-line if space allows (>= 2 lines), else single-line with leading ellipsis
|
||||
const int textX = boxX + 2;
|
||||
const int maxTextWidth = boxWidth - 4;
|
||||
const int maxLines = (boxHeight - 2) / lineH;
|
||||
const int maxLines = (boxHeight - 2) / inputLineH;
|
||||
if (maxLines >= 2) {
|
||||
std::string remaining = inputText;
|
||||
int lineY = boxY + 1;
|
||||
for (int line = 0; line < maxLines && !remaining.empty(); ++line) {
|
||||
int bestLen = 0;
|
||||
for (int len = 1; len <= (int)remaining.size(); ++len) {
|
||||
int w = display->getStringWidth(remaining.substr(0, len).c_str());
|
||||
if (w <= maxTextWidth)
|
||||
bestLen = len;
|
||||
else
|
||||
break;
|
||||
// Inner bounds for caret clamping
|
||||
const int innerLeft = boxX + 1;
|
||||
const int innerRight = boxX + boxWidth - 2;
|
||||
const int innerTop = boxY + 1;
|
||||
const int innerBottom = boxY + boxHeight - 2;
|
||||
|
||||
// Wrap text greedily into lines that fit maxTextWidth
|
||||
std::vector<std::string> lines;
|
||||
{
|
||||
std::string remaining = inputText;
|
||||
while (!remaining.empty()) {
|
||||
int bestLen = 0;
|
||||
for (int len = 1; len <= (int)remaining.size(); ++len) {
|
||||
int w = display->getStringWidth(remaining.substr(0, len).c_str());
|
||||
if (w <= maxTextWidth)
|
||||
bestLen = len;
|
||||
else
|
||||
break;
|
||||
}
|
||||
if (bestLen == 0) {
|
||||
// At least show one character to make progress
|
||||
bestLen = 1;
|
||||
}
|
||||
lines.emplace_back(remaining.substr(0, bestLen));
|
||||
remaining.erase(0, bestLen);
|
||||
}
|
||||
if (bestLen == 0)
|
||||
break;
|
||||
std::string chunk = remaining.substr(0, bestLen);
|
||||
display->drawString(textX, lineY, chunk.c_str());
|
||||
remaining.erase(0, bestLen);
|
||||
lineY += lineH;
|
||||
}
|
||||
// Optional: draw cursor at end of last line could be added if needed
|
||||
|
||||
const bool scrolledUp = ((int)lines.size() > maxLines);
|
||||
int caretX = textX;
|
||||
int caretY = innerTop;
|
||||
|
||||
// Leave a small top gap to render '...' without replacing the first line
|
||||
const int topInset = 2;
|
||||
const int lineStep = std::max(1, inputLineH - 1); // slightly tighter than font height
|
||||
int lineY = innerTop + topInset;
|
||||
|
||||
if (scrolledUp) {
|
||||
// Draw three small dots centered horizontally, vertically at the midpoint of the gap
|
||||
// between the inner top and the first line's top baseline. This avoids using a tall glyph.
|
||||
const int firstLineTop = lineY; // baseline top for the first visible line
|
||||
const int gapMidY = innerTop + (firstLineTop - innerTop) / 2 + 1; // shift down 1px as requested
|
||||
const int centerX = boxX + boxWidth / 2;
|
||||
const int dotSpacing = 3; // px between dots
|
||||
const int dotSize = 1; // small square dot
|
||||
display->fillRect(centerX - dotSpacing, gapMidY, dotSize, dotSize);
|
||||
display->fillRect(centerX, gapMidY, dotSize, dotSize);
|
||||
display->fillRect(centerX + dotSpacing, gapMidY, dotSize, dotSize);
|
||||
}
|
||||
|
||||
// How many lines fit with our top inset and tighter step
|
||||
const int linesCapacity = std::max(1, (innerBottom - lineY + 1) / lineStep);
|
||||
const int linesToShow = std::min((int)lines.size(), linesCapacity);
|
||||
const int startIndex = scrolledUp ? ((int)lines.size() - linesToShow) : 0;
|
||||
|
||||
for (int i = 0; i < linesToShow; ++i) {
|
||||
const std::string &chunk = lines[startIndex + i];
|
||||
display->drawString(textX, lineY, chunk.c_str());
|
||||
caretX = textX + display->getStringWidth(chunk.c_str());
|
||||
caretY = lineY;
|
||||
lineY += lineStep;
|
||||
}
|
||||
|
||||
// Draw caret at end of the last visible line
|
||||
int caretPadY = 2;
|
||||
if (boxHeight >= inputLineH + 4)
|
||||
caretPadY = 3;
|
||||
int cursorTop = caretY + caretPadY;
|
||||
// Use lineStep so caret height matches the row spacing
|
||||
int cursorH = lineStep - caretPadY * 2;
|
||||
if (cursorH < 1)
|
||||
cursorH = 1;
|
||||
// Clamp vertical bounds to stay inside the inner rect
|
||||
if (cursorTop < innerTop)
|
||||
cursorTop = innerTop;
|
||||
if (cursorTop + cursorH - 1 > innerBottom)
|
||||
cursorH = innerBottom - cursorTop + 1;
|
||||
if (cursorH < 1)
|
||||
cursorH = 1;
|
||||
// Only draw if cursor is inside inner bounds
|
||||
if (caretX >= innerLeft && caretX <= innerRight) {
|
||||
display->drawVerticalLine(caretX, cursorTop, cursorH);
|
||||
}
|
||||
} else {
|
||||
std::string displayText = inputText;
|
||||
int textW = display->getStringWidth(displayText.c_str());
|
||||
@@ -230,7 +346,12 @@ void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16
|
||||
const int innerTop = boxY + 1;
|
||||
const int innerBottom = boxY + boxHeight - 2;
|
||||
|
||||
const int textY = boxY + 1;
|
||||
// Position text above vertical center; total up-shift by 4px for single-line
|
||||
int innerH = innerBottom - innerTop + 1;
|
||||
int textY = innerTop + std::max(0, (innerH - inputLineH) / 2) - 5; // was -4, now -5
|
||||
// Allow clamping to the outer border so upward shift remains visible on very small boxes
|
||||
if (textY < boxY)
|
||||
textY = boxY;
|
||||
if (!scrolled.empty()) {
|
||||
display->drawString(textX, textY, scrolled.c_str());
|
||||
}
|
||||
@@ -240,12 +361,9 @@ void VirtualKeyboard::drawInputArea(OLEDDisplay *display, int16_t offsetX, int16
|
||||
if (cursorX > innerRight)
|
||||
cursorX = innerRight;
|
||||
|
||||
// Apply vertical padding so caret doesn't touch top/bottom
|
||||
int caretPadY = 2;
|
||||
if (boxHeight >= lineH + 4)
|
||||
caretPadY = 3; // if box is taller than minimal, allow larger gap
|
||||
int cursorTop = textY + caretPadY;
|
||||
int cursorH = lineH - caretPadY * 2;
|
||||
// Caret: height = outer box height - 4, with a 2px margin from top/bottom
|
||||
int cursorTop = boxY + 2;
|
||||
int cursorH = boxHeight - 4;
|
||||
if (cursorH < 1)
|
||||
cursorH = 1;
|
||||
// Clamp vertical bounds to stay inside the inner rect
|
||||
@@ -268,7 +386,7 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool
|
||||
{
|
||||
// Draw key content
|
||||
display->setFont(FONT_SMALL);
|
||||
const int fontH = FONT_HEIGHT_SMALL; // actual pixel height of current font
|
||||
const int fontH = FONT_HEIGHT_SMALL;
|
||||
// Build label and metrics first
|
||||
std::string keyText;
|
||||
if (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC) {
|
||||
@@ -294,32 +412,16 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool
|
||||
if (textX < x)
|
||||
textX = x; // guard
|
||||
} else {
|
||||
textX = x + ((width - textWidth) + 1) / 2; // ceil((w - tw)/2)
|
||||
textX = x + (width - textWidth) / 2;
|
||||
}
|
||||
int textY = y + (height - fontH) / 2; // baseline for text
|
||||
// Per-character vertical nudge for better visual centering (only for single-character keys)
|
||||
if (key.type == VK_CHAR) {
|
||||
int nudge = 0;
|
||||
if (keyText == "j") {
|
||||
nudge = 1; // j up 1px
|
||||
} else if (keyText.find_first_of("gpqy") != std::string::npos) {
|
||||
nudge = 2; // g/p/q/y up 2px
|
||||
} else if (keyText == ";" || keyText == "." || keyText == ",") {
|
||||
nudge = 1; // ; . , up 1px to appear vertically centered
|
||||
}
|
||||
if (nudge) {
|
||||
textY -= nudge;
|
||||
if (textY < 0)
|
||||
textY = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Selected: for action text buttons, highlight fits text width; for char keys, fill entire key
|
||||
int contentTop = y;
|
||||
int contentH = height;
|
||||
if (selected) {
|
||||
display->setColor(WHITE);
|
||||
bool isAction = (key.type == VK_BACKSPACE || key.type == VK_ENTER || key.type == VK_SPACE || key.type == VK_ESC);
|
||||
if (isAction) {
|
||||
const int padX = 2; // small horizontal padding around text
|
||||
const int padY = 1; // vertical padding so highlight doesn't touch edges
|
||||
int hlX = textX - padX;
|
||||
int hlW = textWidth + padX * 2;
|
||||
// Constrain highlight within the key's horizontal span
|
||||
@@ -333,16 +435,45 @@ void VirtualKeyboard::drawKey(OLEDDisplay *display, const VirtualKey &key, bool
|
||||
hlW = maxW;
|
||||
if (hlW < 1)
|
||||
hlW = 1;
|
||||
display->fillRect(hlX, y, hlW, height);
|
||||
// Vertical: keep a small gap from top/bottom to avoid overlap with neighboring rows
|
||||
int hlY = y + padY;
|
||||
int hlH = height - padY * 2 + 2; // extend downward by 1px
|
||||
if (hlH < 1)
|
||||
hlH = 1;
|
||||
display->fillRect(hlX, hlY, hlW, hlH);
|
||||
// Content area (for centering text) matches highlight
|
||||
contentTop = hlY;
|
||||
contentH = hlH;
|
||||
} else {
|
||||
display->fillRect(x, y, width, height);
|
||||
int hlY = y + 1;
|
||||
int hlH = height + 1;
|
||||
if (hlH < 1)
|
||||
hlH = 1;
|
||||
display->fillRect(x, hlY, width, hlH);
|
||||
contentTop = hlY;
|
||||
contentH = hlH;
|
||||
}
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
|
||||
display->drawString(textX, textY, keyText.c_str());
|
||||
int centeredTextY = contentTop + (contentH - fontH) / 2;
|
||||
if (key.type == VK_CHAR) {
|
||||
if (keyText.size() == 1) {
|
||||
char ch = keyText[0];
|
||||
bool tinyScreen = (display->getHeight() <= 64);
|
||||
if (tinyScreen) {
|
||||
if (ch == 'g' || ch == 'j' || ch == 'q' || ch == 'y' || ch == 'p' || ch == 'v' || ch == '.' || ch == ',' ||
|
||||
ch == ';') {
|
||||
centeredTextY -= 1;
|
||||
if (centeredTextY < 0)
|
||||
centeredTextY = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
display->drawString(textX, centeredTextY, keyText.c_str());
|
||||
}
|
||||
|
||||
char VirtualKeyboard::getCharForKey(const VirtualKey &key, bool isLongPress)
|
||||
@@ -454,10 +585,14 @@ void VirtualKeyboard::handleLongPress()
|
||||
return;
|
||||
}
|
||||
|
||||
// For non-character keys, long press behaves the same as regular press
|
||||
switch (key.type) {
|
||||
case VK_BACKSPACE:
|
||||
deleteCharacter();
|
||||
// One-shot: delete up to 5 characters on long press
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
if (inputText.empty())
|
||||
break;
|
||||
deleteCharacter();
|
||||
}
|
||||
break;
|
||||
case VK_ENTER:
|
||||
submitText();
|
||||
|
||||
Reference in New Issue
Block a user