Fullscreen keyboard dialog, nearest-cell vertical navigation, overwrite-at-max-length

- Keyboard dialog now fills the whole screen (no dimming backdrop); key
  cells use a fixed font-tile-based size instead of scaling to a
  percentage of the screen.
- uiMenuFocusSkipBlanks: UP/DOWN now search every non-blank cell strictly
  further in the pressed direction and land on the smallest combined
  row+column distance (ties favor the smaller column distance), instead
  of only scanning straight down the same column. Neither axis wraps
  during that search - only once the edge row is reached with nothing
  further to search does it fall back to wrapping to the opposite edge.
- Typing a new character while the keyboard's text is already at
  maxLength now overrides the last character instead of being ignored.
This commit is contained in:
2026-08-27 21:14:19 -05:00
parent 00bfaf6360
commit 4387d223b9
3 changed files with 156 additions and 66 deletions
+41 -38
View File
@@ -17,11 +17,13 @@
#include "display/color.h" #include "display/color.h"
#include "display/text/font.h" #include "display/text/font.h"
#include "display/spritebatch/spritebatch.h" #include "display/spritebatch/spritebatch.h"
#include "display/texture/texture.h"
#include "display/shader/shaderunlit.h"
#define UI_KEYBOARD_BACKDROP_COLOR color4b(0, 0, 0, 160) // Key cell size, expressed in font tiles so it scales if the font ever
#define UI_KEYBOARD_KEY_WIDTH 20.0f // does. Row height is derived from this ratio in uiKeyboardDraw rather
// than being its own fixed constant, so a key cell keeps its proportions
// even when contentWidth is stretched wider by a long title/text label.
#define UI_KEYBOARD_KEY_WIDTH ((float_t)(FONT_DEFAULT_TILE_WIDTH * 5))
#define UI_KEYBOARD_KEY_HEIGHT ((float_t)(FONT_DEFAULT_TILE_HEIGHT * 2))
uikeyboard_t UI_KEYBOARD; uikeyboard_t UI_KEYBOARD;
@@ -184,7 +186,14 @@ bool_t uiKeyboardTextIsBlank(void) {
void uiKeyboardAppendChar(const char_t c) { void uiKeyboardAppendChar(const char_t c) {
size_t length = strlen(UI_KEYBOARD.text); size_t length = strlen(UI_KEYBOARD.text);
if(length >= UI_KEYBOARD.maxLength) return;
// Full - drop the last character instead of ignoring the keypress, so
// the new one overrides it. Falls through to the normal append below,
// which now has room again.
if(length >= UI_KEYBOARD.maxLength) {
length--;
UI_KEYBOARD.text[length] = '\0';
}
if(c == '\n') { if(c == '\n') {
uint8_t lines = 1; uint8_t lines = 1;
@@ -456,22 +465,12 @@ void uiKeyboardClose(void) {
errorret_t uiKeyboardDraw(void) { errorret_t uiKeyboardDraw(void) {
if(!UI_KEYBOARD.open) errorOk(); if(!UI_KEYBOARD.open) errorOk();
spritebatchsprite_t backdropSprite = { float_t x = (float_t)SCREEN.scanX;
.min = { 0.0f, 0.0f, 0.0f }, float_t y = (float_t)SCREEN.scanY;
.max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f }, float_t width = (float_t)SCREEN.scanWidth;
.uvMin = { 0.0f, 0.0f }, float_t height = (float_t)SCREEN.scanHeight;
.uvMax = { 1.0f, 1.0f }
}; errorChain(uiFrameDraw(x, y, width, height));
shadermaterial_t backdropMaterial = {
.unlit = {
.color = UI_KEYBOARD_BACKDROP_COLOR,
.texture = &TEXTURE_WHITE
}
};
errorChain(
spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial)
);
errorChain(spriteBatchFlush());
float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight; float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
// When multi-line, reserve the full lineCount rows up front so the // When multi-line, reserve the full lineCount rows up front so the
@@ -481,28 +480,23 @@ errorret_t uiKeyboardDraw(void) {
float_t textHeight = UI_KEYBOARD.lineCount > 1 ? float_t textHeight = UI_KEYBOARD.lineCount > 1 ?
(float_t)UI_KEYBOARD.lineCount * rowHeight : (float_t)UI_KEYBOARD.lineCount * rowHeight :
mathMax(rowHeight, (float_t)UI_KEYBOARD.textLabel.height); mathMax(rowHeight, (float_t)UI_KEYBOARD.textLabel.height);
uint8_t rows = (UI_KEYBOARD.menu.itemCount + UI_KEYBOARD.menu.columns - 1) / uint8_t columns = UI_KEYBOARD.menu.columns;
UI_KEYBOARD.menu.columns; uint8_t rows = (UI_KEYBOARD.menu.itemCount + columns - 1) / columns;
float_t contentWidth = mathMax( float_t contentWidth = mathMax(
mathMax((float_t)UI_KEYBOARD.titleLabel.width, (float_t)UI_KEYBOARD.textLabel.width), mathMax((float_t)UI_KEYBOARD.titleLabel.width, (float_t)UI_KEYBOARD.textLabel.width),
(float_t)UI_KEYBOARD.menu.columns * UI_KEYBOARD_KEY_WIDTH (float_t)columns * UI_KEYBOARD_KEY_WIDTH
); );
float_t width = contentWidth + (UI_FRAME_START_X * 2); float_t colStep = contentWidth / (float_t)columns;
float_t height = (UI_FRAME_START_Y * 2) float_t keyRowHeight = colStep * (UI_KEYBOARD_KEY_HEIGHT / UI_KEYBOARD_KEY_WIDTH);
+ rowHeight + UI_FRAME_PADDING_Y float_t contentHeight = rowHeight + UI_FRAME_PADDING_Y
+ textHeight + UI_FRAME_PADDING_Y + textHeight + UI_FRAME_PADDING_Y
+ ((float_t)rows * rowHeight); + ((float_t)rows * keyRowHeight);
float_t x = (float_t)SCREEN.scanX + // The frame now fills the whole screen, but the title/text/keys still
((float_t)SCREEN.scanWidth - width) * 0.5f; // lay out as one content block - center that block within the screen.
float_t y = (float_t)SCREEN.scanY + float_t contentX = x + (width - contentWidth) * 0.5f;
((float_t)SCREEN.scanHeight - height) * 0.5f; float_t contentY = y + (height - contentHeight) * 0.5f;
errorChain(uiFrameDraw(x, y, width, height));
float_t contentX = x + UI_FRAME_START_X;
float_t contentY = y + UI_FRAME_START_Y;
uiLabelSetX(&UI_KEYBOARD.titleLabel, contentX); uiLabelSetX(&UI_KEYBOARD.titleLabel, contentX);
uiLabelSetY(&UI_KEYBOARD.titleLabel, contentY); uiLabelSetY(&UI_KEYBOARD.titleLabel, contentY);
@@ -513,8 +507,17 @@ errorret_t uiKeyboardDraw(void) {
uiLabelSetY(&UI_KEYBOARD.textLabel, textY); uiLabelSetY(&UI_KEYBOARD.textLabel, textY);
errorChain(uiLabelRender(&UI_KEYBOARD.textLabel, COLOR_WHITE)); errorChain(uiLabelRender(&UI_KEYBOARD.textLabel, COLOR_WHITE));
// Drawn directly rather than via uiMenuDraw - that helper hardcodes row
// spacing to the font's native tile height, which would keep the grid
// vertically compact no matter how much contentWidth grows. Focus
// highlighting is unaffected: it's applied to item->button.highlighted
// by the focus system independently of whoever calls uiButtonDraw.
float_t menuY = textY + textHeight + UI_FRAME_PADDING_Y; float_t menuY = textY + textHeight + UI_FRAME_PADDING_Y;
errorChain(uiMenuDraw(&UI_KEYBOARD.menu, contentX, menuY, contentWidth, rowHeight)); for(uint8_t i = 0; i < UI_KEYBOARD.menu.itemCount; i++) {
float_t ix = contentX + (float_t)(i % columns) * colStep;
float_t iy = menuY + (float_t)(i / columns) * keyRowHeight;
errorChain(uiButtonDraw(&UI_KEYBOARD.items[i].button, ix, iy));
}
errorChain(spriteBatchFlush()); errorChain(spriteBatchFlush());
errorOk(); errorOk();
+90 -22
View File
@@ -293,34 +293,102 @@ bool_t uiMenuFocusSkipBlanks(
} }
if(dx == 0 && dy == 0) return false; if(dx == 0 && dy == 0) return false;
// Only one of dx/dy is ever non-zero (a direction moves along a single // Horizontal: a straight scan along the row. There's no "nearest"
// axis), so the number of distinct cells reachable before this would // ambiguity here - the next non-blank cell in the pressed direction is
// cycle back to the start is exactly that axis's length. // always the right one, wrapping within the row as needed.
uint8_t attempts = dx != 0 ? menu->columns : focusItem->rows; if(dy == 0) {
int16_t x = focusItem->x;
for(uint8_t i = 0; i < menu->columns; i++) {
x += dx;
if(x < 0) x += menu->columns;
x %= menu->columns;
int16_t x = focusItem->x; uint8_t slot = focusItem->y * menu->columns + (uint8_t)x;
int16_t y = focusItem->y; uint8_t index = uiMenuFocusSlotToIndex(menu, slot);
// Treat a missing item (label/spacer slot) the same as a blank
// button - both get skipped over transparently.
if(index != 0xFF && !uiMenuItemIsBlank(&menu->items[index])) {
uiMenuSetPosition(menu, (uint8_t)x, focusItem->y);
return true;
}
}
return false;
}
for(uint8_t i = 0; i < attempts; i++) { // Vertical: search every cell strictly further in the pressed
x += dx; // direction (no wrap - the rows below are only ever "below", never
y += dy; // "above via the far edge") and land on whichever non-blank cell has
if(x < 0) x += menu->columns; // the smallest combined row+column distance from where we moved from.
if(y < 0) y += focusItem->rows; // This lets an exact column match several rows away beat an
x %= menu->columns; // approximate match one row away, while still reaching something like
y %= focusItem->rows; // SPACE from a key with nothing directly below it. Ties on total
// distance favor the smaller column distance - e.g. moving straight
// down two rows onto an exact column match beats landing one row down
// one column over, even though both are three cells away.
int16_t rowStart = dy > 0 ? focusItem->y + 1 : 0;
int16_t rowEnd = dy > 0 ? focusItem->rows - 1 : focusItem->y - 1;
uint8_t slot = (uint8_t)y * menu->columns + (uint8_t)x; bool_t found = false;
uint8_t index = uiMenuFocusSlotToIndex(menu, slot); uint16_t bestDistance = 0;
// Treat a missing item (label/spacer slot) the same as a blank uint8_t bestColDistance = 0;
// button - both get skipped over transparently. uint8_t bestX = 0;
if(index != 0xFF && !uiMenuItemIsBlank(&menu->items[index])) { uint8_t bestY = 0;
uiMenuSetPosition(menu, (uint8_t)x, (uint8_t)y);
return true; for(int16_t yy = rowStart; yy <= rowEnd; yy++) {
uint8_t rowDistance = dy > 0 ?
(uint8_t)(yy - focusItem->y) : (uint8_t)(focusItem->y - yy);
for(uint8_t xx = 0; xx < menu->columns; xx++) {
uint8_t colDistance = xx > focusItem->x ?
xx - focusItem->x : focusItem->x - xx;
uint16_t distance = rowDistance + colDistance;
if(found && distance > bestDistance) continue;
if(found && distance == bestDistance && colDistance >= bestColDistance) {
continue;
}
uint8_t slot = (uint8_t)yy * menu->columns + xx;
uint8_t index = uiMenuFocusSlotToIndex(menu, slot);
if(index == 0xFF || uiMenuItemIsBlank(&menu->items[index])) continue;
found = true;
bestDistance = distance;
bestColDistance = colDistance;
bestX = xx;
bestY = (uint8_t)yy;
} }
} }
// Every cell along this line is blank/missing - nothing to land on, // Nothing further in that direction at all (already on the edge row) -
// fall through to the default single-step move. // wrap to the opposite edge as a last resort, landing on its nearest
// column. This only ever kicks in when the non-wrapped search above
// found literally nothing, so it can't hijack a real nearby match the
// way wrapping during the main search could.
if(!found && focusItem->rows > 1) {
uint8_t wrapY = dy > 0 ? 0 : focusItem->rows - 1;
for(uint8_t xx = 0; xx < menu->columns; xx++) {
uint8_t colDistance = xx > focusItem->x ?
xx - focusItem->x : focusItem->x - xx;
if(found && colDistance >= bestDistance) continue;
uint8_t slot = wrapY * menu->columns + xx;
uint8_t index = uiMenuFocusSlotToIndex(menu, slot);
if(index == 0xFF || uiMenuItemIsBlank(&menu->items[index])) continue;
found = true;
bestDistance = colDistance;
bestX = xx;
bestY = wrapY;
}
}
if(found) {
uiMenuSetPosition(menu, bestX, bestY);
return true;
}
// Nothing non-blank in that direction at all - fall through to the
// default single-step move.
return false; return false;
} }
+25 -6
View File
@@ -271,12 +271,31 @@ bool_t uiMenuFocusDirection(
bool_t uiMenuItemIsBlank(const uimenuitem_t *item); bool_t uiMenuItemIsBlank(const uimenuitem_t *item);
/** /**
* Moves focusItem to the next cell in the given direction that isn't * Moves focusItem to a non-blank cell (see uiMenuItemIsBlank) in the
* blank (see uiMenuItemIsBlank), wrapping within that row/column as many * given direction, wrapping the grid as many times as needed - so, from
* times as needed - so, from the menu's perspective, a blank cell simply * the menu's perspective, a blank cell simply isn't there.
* isn't there. Gives up (returning false, so the default single-step *
* move applies instead) only if every cell along that entire row/column * LEFT/RIGHT scan straight along the row, since there's no ambiguity
* is blank. * about which non-blank cell is "next" (wrapping within the row as
* needed). UP/DOWN instead considers every cell strictly further in the
* pressed direction and lands on whichever non-blank cell has the
* smallest combined row+column distance from the one moved from - e.g.
* moving down off a densely-packed row onto a mostly-blank one lands on
* whatever's closest instead of only ever reaching a cell that happens
* to share the exact same column, while an exact column match a couple
* of rows down still wins over an approximate match one row down. Ties
* on total distance favor the smaller column distance, so a straight
* move down onto an exact column match beats an equally-far diagonal
* one. This search never wraps in either axis while there's still a
* real cell further in the pressed direction - column/row 0 is never
* treated as adjacent to the far edge just because the grid wraps in
* general. Only once already on the edge row (nothing further to search
* at all) does it fall back to wrapping to the opposite edge's nearest
* column, so pressing up from the top row still goes somewhere instead
* of doing nothing.
*
* Gives up (returning false, so the default single-step move applies
* instead) only if every cell on the grid is blank.
* *
* @param menu The menu to move within. * @param menu The menu to move within.
* @param focusItem The active focus item; must belong to menu. * @param focusItem The active focus item; must belong to menu.