From 4387d223b9ba27a0bc13f794287236909c03e354 Mon Sep 17 00:00:00 2001 From: Dominic Masters Date: Thu, 27 Aug 2026 21:14:19 -0500 Subject: [PATCH] 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. --- src/dusk/ui/dialog/keyboard/uikeyboard.c | 79 ++++++++-------- src/dusk/ui/widget/uimenu.c | 112 ++++++++++++++++++----- src/dusk/ui/widget/uimenu.h | 31 +++++-- 3 files changed, 156 insertions(+), 66 deletions(-) diff --git a/src/dusk/ui/dialog/keyboard/uikeyboard.c b/src/dusk/ui/dialog/keyboard/uikeyboard.c index 8a149827..f778af71 100644 --- a/src/dusk/ui/dialog/keyboard/uikeyboard.c +++ b/src/dusk/ui/dialog/keyboard/uikeyboard.c @@ -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(); diff --git a/src/dusk/ui/widget/uimenu.c b/src/dusk/ui/widget/uimenu.c index 4728295a..38f957fc 100644 --- a/src/dusk/ui/widget/uimenu.c +++ b/src/dusk/ui/widget/uimenu.c @@ -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; } diff --git a/src/dusk/ui/widget/uimenu.h b/src/dusk/ui/widget/uimenu.h index 74a8defd..594e4377 100644 --- a/src/dusk/ui/widget/uimenu.h +++ b/src/dusk/ui/widget/uimenu.h @@ -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.