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:
@@ -17,11 +17,13 @@
|
||||
#include "display/color.h"
|
||||
#include "display/text/font.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)
|
||||
#define UI_KEYBOARD_KEY_WIDTH 20.0f
|
||||
// Key cell size, expressed in font tiles so it scales if the font ever
|
||||
// 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;
|
||||
|
||||
@@ -184,7 +186,14 @@ bool_t uiKeyboardTextIsBlank(void) {
|
||||
|
||||
void uiKeyboardAppendChar(const char_t c) {
|
||||
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') {
|
||||
uint8_t lines = 1;
|
||||
@@ -456,22 +465,12 @@ void uiKeyboardClose(void) {
|
||||
errorret_t uiKeyboardDraw(void) {
|
||||
if(!UI_KEYBOARD.open) errorOk();
|
||||
|
||||
spritebatchsprite_t backdropSprite = {
|
||||
.min = { 0.0f, 0.0f, 0.0f },
|
||||
.max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f },
|
||||
.uvMin = { 0.0f, 0.0f },
|
||||
.uvMax = { 1.0f, 1.0f }
|
||||
};
|
||||
shadermaterial_t backdropMaterial = {
|
||||
.unlit = {
|
||||
.color = UI_KEYBOARD_BACKDROP_COLOR,
|
||||
.texture = &TEXTURE_WHITE
|
||||
}
|
||||
};
|
||||
errorChain(
|
||||
spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial)
|
||||
);
|
||||
errorChain(spriteBatchFlush());
|
||||
float_t x = (float_t)SCREEN.scanX;
|
||||
float_t y = (float_t)SCREEN.scanY;
|
||||
float_t width = (float_t)SCREEN.scanWidth;
|
||||
float_t height = (float_t)SCREEN.scanHeight;
|
||||
|
||||
errorChain(uiFrameDraw(x, y, width, height));
|
||||
|
||||
float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
|
||||
// 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)UI_KEYBOARD.lineCount * rowHeight :
|
||||
mathMax(rowHeight, (float_t)UI_KEYBOARD.textLabel.height);
|
||||
uint8_t rows = (UI_KEYBOARD.menu.itemCount + UI_KEYBOARD.menu.columns - 1) /
|
||||
UI_KEYBOARD.menu.columns;
|
||||
uint8_t columns = UI_KEYBOARD.menu.columns;
|
||||
uint8_t rows = (UI_KEYBOARD.menu.itemCount + columns - 1) / columns;
|
||||
|
||||
float_t contentWidth = mathMax(
|
||||
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 height = (UI_FRAME_START_Y * 2)
|
||||
+ rowHeight + UI_FRAME_PADDING_Y
|
||||
float_t colStep = contentWidth / (float_t)columns;
|
||||
float_t keyRowHeight = colStep * (UI_KEYBOARD_KEY_HEIGHT / UI_KEYBOARD_KEY_WIDTH);
|
||||
float_t contentHeight = rowHeight + UI_FRAME_PADDING_Y
|
||||
+ textHeight + UI_FRAME_PADDING_Y
|
||||
+ ((float_t)rows * rowHeight);
|
||||
+ ((float_t)rows * keyRowHeight);
|
||||
|
||||
float_t x = (float_t)SCREEN.scanX +
|
||||
((float_t)SCREEN.scanWidth - width) * 0.5f;
|
||||
float_t y = (float_t)SCREEN.scanY +
|
||||
((float_t)SCREEN.scanHeight - height) * 0.5f;
|
||||
|
||||
errorChain(uiFrameDraw(x, y, width, height));
|
||||
|
||||
float_t contentX = x + UI_FRAME_START_X;
|
||||
float_t contentY = y + UI_FRAME_START_Y;
|
||||
// The frame now fills the whole screen, but the title/text/keys still
|
||||
// lay out as one content block - center that block within the screen.
|
||||
float_t contentX = x + (width - contentWidth) * 0.5f;
|
||||
float_t contentY = y + (height - contentHeight) * 0.5f;
|
||||
|
||||
uiLabelSetX(&UI_KEYBOARD.titleLabel, contentX);
|
||||
uiLabelSetY(&UI_KEYBOARD.titleLabel, contentY);
|
||||
@@ -513,8 +507,17 @@ errorret_t uiKeyboardDraw(void) {
|
||||
uiLabelSetY(&UI_KEYBOARD.textLabel, textY);
|
||||
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;
|
||||
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());
|
||||
errorOk();
|
||||
|
||||
+90
-22
@@ -293,34 +293,102 @@ bool_t uiMenuFocusSkipBlanks(
|
||||
}
|
||||
if(dx == 0 && dy == 0) return false;
|
||||
|
||||
// Only one of dx/dy is ever non-zero (a direction moves along a single
|
||||
// axis), so the number of distinct cells reachable before this would
|
||||
// cycle back to the start is exactly that axis's length.
|
||||
uint8_t attempts = dx != 0 ? menu->columns : focusItem->rows;
|
||||
// Horizontal: a straight scan along the row. There's no "nearest"
|
||||
// ambiguity here - the next non-blank cell in the pressed direction is
|
||||
// always the right one, wrapping within the row as needed.
|
||||
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;
|
||||
int16_t y = focusItem->y;
|
||||
uint8_t slot = focusItem->y * menu->columns + (uint8_t)x;
|
||||
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++) {
|
||||
x += dx;
|
||||
y += dy;
|
||||
if(x < 0) x += menu->columns;
|
||||
if(y < 0) y += focusItem->rows;
|
||||
x %= menu->columns;
|
||||
y %= focusItem->rows;
|
||||
// Vertical: search every cell strictly further in the pressed
|
||||
// direction (no wrap - the rows below are only ever "below", never
|
||||
// "above via the far edge") and land on whichever non-blank cell has
|
||||
// the smallest combined row+column distance from where we moved from.
|
||||
// This lets an exact column match several rows away beat an
|
||||
// approximate match one row away, while still reaching something like
|
||||
// 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;
|
||||
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, (uint8_t)y);
|
||||
return true;
|
||||
bool_t found = false;
|
||||
uint16_t bestDistance = 0;
|
||||
uint8_t bestColDistance = 0;
|
||||
uint8_t bestX = 0;
|
||||
uint8_t bestY = 0;
|
||||
|
||||
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,
|
||||
// fall through to the default single-step move.
|
||||
// Nothing further in that direction at all (already on the edge row) -
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -271,12 +271,31 @@ bool_t uiMenuFocusDirection(
|
||||
bool_t uiMenuItemIsBlank(const uimenuitem_t *item);
|
||||
|
||||
/**
|
||||
* Moves focusItem to the next cell in the given direction that isn't
|
||||
* blank (see uiMenuItemIsBlank), wrapping within that row/column as many
|
||||
* times as needed - so, from the menu's perspective, a blank cell simply
|
||||
* isn't there. Gives up (returning false, so the default single-step
|
||||
* move applies instead) only if every cell along that entire row/column
|
||||
* is blank.
|
||||
* Moves focusItem to a non-blank cell (see uiMenuItemIsBlank) in the
|
||||
* given direction, wrapping the grid as many times as needed - so, from
|
||||
* the menu's perspective, a blank cell simply isn't there.
|
||||
*
|
||||
* LEFT/RIGHT scan straight along the row, since there's no ambiguity
|
||||
* 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 focusItem The active focus item; must belong to menu.
|
||||
|
||||
Reference in New Issue
Block a user